diff --git a/.codecov.yml b/.codecov.yml index d8bafa304..8fcaf7415 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,4 +1,4 @@ -comment: off +comment: false coverage: status: @@ -7,5 +7,5 @@ coverage: target: 90 threshold: null base: auto - changes: off - patch: off + changes: false + patch: false diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..45207b077 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "name": "TypeScriptToLua", + "image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:18", + "extensions": [ + "typescript-to-lua.vscode-typescript-to-lua", + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "esbenp.prettier-vscode" + ], + "postCreateCommand": "npm ci", + "remoteUser": "node" +} diff --git a/.editorconfig b/.editorconfig index 69fbfa8bc..ceeeb10cd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,5 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.yml] +[*.{yml,md}] indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..cd3dd9f16 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# Sponsoring TypeScriptToLua helps us stay motivated and shows your appreciation for our work! +github: [Perryvw] diff --git a/.github/scripts/create_benchmark_check.js b/.github/scripts/create_benchmark_check.js new file mode 100644 index 000000000..4d9ac86cc --- /dev/null +++ b/.github/scripts/create_benchmark_check.js @@ -0,0 +1,28 @@ +module.exports = ({ github, context, core }) => { + const fs = require("fs"); + + const benchmarkResultPathLua = core.getInput("benchmark-result-path-lua", { required: true }); + const benchmarkInfoLua = JSON.parse(fs.readFileSync(benchmarkResultPathLua)); + + const benchmarkResultPathJIT = core.getInput("benchmark-result-path-jit", { required: true }); + const benchmarkInfoJIT = JSON.parse(fs.readFileSync(benchmarkResultPathJIT)); + + // Remove Comparison info to save some bytes + const benchmarkInfoForVizLua = { old: benchmarkInfoLua.old, new: benchmarkInfoLua.new }; + const buffer = Buffer.from(JSON.stringify(benchmarkInfoForVizLua)); + + const zlib = require("zlib"); + const compressed = zlib.deflateSync(buffer); + + const summary = + `[Open visualizer](https://typescripttolua.github.io/benchviz?d=${compressed.toString("base64")})\n` + + `### Lua5.3 +${benchmarkInfoLua.comparison.memory.summary} +${benchmarkInfoLua.comparison.runtime.summary} +--- +### LuaJIT +${benchmarkInfoJIT.comparison.memory.summary} +${benchmarkInfoJIT.comparison.runtime.summary}`; + + return summary; +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..8353ca6d4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,139 @@ +name: CI + +on: + push: + branches: master + pull_request: + +env: + NODE_VERSION: 20.17.0 + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - run: npm ci + - run: npm run lint + env: + CI: true + + test: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 16.14.0 + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - run: npm ci + - run: npm run build + - run: npx jest --maxWorkers 2 --coverage + env: + CI: true + - if: matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@v4 + + benchmark: + name: Benchmark + runs-on: ubuntu-latest + steps: + - name: Lua Install + run: sudo apt-get install lua5.3 luajit + - name: Add Brew to Path (Required since Nov 2022) + run: echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH + - name: Glow Install + run: brew install glow + # Checkout master & commit + - name: Checkout master + uses: actions/checkout@v4 + with: + ref: master + path: master + - name: Checkout commit + uses: actions/checkout@v4 + with: + path: commit + - name: Use Node.js 16.14.0 + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + # NPM + # install and build master + - name: npm ci master + run: npm ci + working-directory: master + - name: Use local tstl language extensions + run: npm i lua-types@latest && npm i -D file:. + working-directory: master + - name: Build master + run: npm run build + working-directory: master + # install and build commit + - name: npm ci commit + run: npm ci + working-directory: commit + - name: Use local tstl language extensions + run: npm i -D file:. + working-directory: commit + - name: Build commit + run: npm run build + working-directory: commit + # Benchmark directory setup + - name: Ensure benchmark data dir exists + run: mkdir -p ./benchmark/data + working-directory: commit + - name: Copy commit benchmark to master + run: rm -rf ./master/benchmark && cp -rf ./commit/benchmark ./master/benchmark + # Run master benchmark first and output to commit benchmark data + - name: Build benchmark Lua 5.3 master + run: node ../dist/tstl.js -p tsconfig.53.json + working-directory: master/benchmark + - name: Run benchmark Lua 5.3 master + id: benchmark-lua-master + run: lua5.3 -- run.lua ../../../commit/benchmark/data/benchmark_master_53.json + working-directory: master/benchmark/dist + - name: Build benchmark LuaJIT master + run: node ../dist/tstl.js -p tsconfig.jit.json + working-directory: master/benchmark + - name: Run benchmark LuaJIT master + id: benchmark-jit-master + run: luajit -- run.lua ../../../commit/benchmark/data/benchmark_master_jit.json + working-directory: master/benchmark/dist + # Run commit benchmark and compare with master + - name: Build benchmark Lua 5.3 commit + run: node ../dist/tstl.js -p tsconfig.53.json + working-directory: commit/benchmark + - name: Run benchmark Lua 5.3 commit + id: benchmark-lua-commit + run: lua5.3 -- run.lua ../data/benchmark_master_vs_commit_53.json ../data/benchmark_master_53.json + working-directory: commit/benchmark/dist + - name: Build benchmark LuaJIT commit + run: node ../dist/tstl.js -p tsconfig.jit.json + working-directory: commit/benchmark + - name: Run benchmark LuaJIT commit + id: benchmark-jit-commit + run: luajit -- run.lua ../data/benchmark_master_vs_commit_jit.json ../data/benchmark_master_jit.json + working-directory: commit/benchmark/dist + - name: Combine benchmark results + id: script-combine-results + uses: actions/github-script@v7 + with: + benchmark-result-path-lua: commit/benchmark/data/benchmark_master_vs_commit_53.json + benchmark-result-path-jit: commit/benchmark/data/benchmark_master_vs_commit_jit.json + result-encoding: string + script: | + const createBenchmarkCheck = require(`${process.env.GITHUB_WORKSPACE}/commit/.github/scripts/create_benchmark_check.js`); + return createBenchmarkCheck({ github, context, core }); + - name: Benchmark results + run: echo "${{steps.script-combine-results.outputs.result}}" | glow -s dark -w 120 - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..f8146b8cb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release + +on: + push: + tags: "*" + +permissions: + id-token: write + contents: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 24.12.0 + uses: actions/setup-node@v1 + with: + node-version: 24.12.0 + registry-url: "https://registry.npmjs.org" + - run: npm ci + - run: npm run build + - run: npm publish diff --git a/.gitignore b/.gitignore index 987b45888..fc3920330 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,17 @@ -*.js -node_modules/ -yarn.lock - -.vscode/ - -coverage/ -.nyc* -*.js.map +node_modules +/dist +/coverage +/test/transpile/module-resolution/**/dist +/test/transpile/module-resolution/**/tsconfig.tsbuildinfo -# Release -*.tgz - -# OSX +yarn.lock +.vscode +.idea .DS_Store -*.lcov - -# IDEA IDEs -.idea/ -typescript_lualib.lua -lualib_bundle.lua +benchmark/data/* +benchmark/dist/* -dist/* +# v8 cpu profiles +*-.log +*.cpuprofile diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..fb6d91319 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +/dist +/coverage +/test/translation/transformation/characterEscapeSequence.ts +/test/translation/transformation/exportStatement.ts +/benchmark/dist +/test/transpile/module-resolution/**/node_modules +/test/transpile/module-resolution/**/dist diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..9dd6427dc --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "arrowParens": "avoid", + "overrides": [{ "files": ["**/*.md", "**/*.yml"], "options": { "tabWidth": 2 } }] +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 72862aafe..000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: node_js -node_js: -- stable -install: -- npm install -script: -- npm run build -- npm run coverage -after_success: -- codecov -deploy: - provider: npm - email: lorenz.junglas@student.kit.edu - skip_cleanup: true - api_key: - secure: zXFXK1GohutgqKXQ101uKcJKcf0wgCuVPFMY44Xl/JwXeLsA1CJ4aqUdoXh2ramcI+9wTVj7RR3N3/7W+2DeDCfUlj+gxw26pTV3EUnU2B1TXEz01Ar07WPHmhHJSS5tgyo9kPUqm/YBFkJuDtfx0Kp3sL6IRYDddvGv/2L1X8r3I14PxgppkU9T4FGINdClOEwfwelmxx3kjxpnTsUHZ+ztx7o+blteSf/r+pT1RDaIap80JaTy6vHwJKECRSvtpyTKEYeVmBIaA3yXqRhfMdlp8bU20t2DXoDGXWPk0GXRGXkVHE6yj64gPe/eOOesAxzv0v/vr5w4poemJ+SGwP3RSbrbVtiusD4FC6+hFBH7hiNUhxww5euvbAfNOTq2XyxtjcNmKB5/O675xGihK1gBgrsPdJ4enwkhQNrUuQBHm5wIKGiaH7t7q+T8W9JAnk3FGGuSZPg9b7AnFDQ3graxZK9mtOxi0GvE7DHinH6qErd4noGjS/KSy1fnDkCEkeplOQmvxb4w7wLMR5pePFc77NBXiR3RJDKh4QKskcvXPx58OWNffTkS2QwYPYhraNNKbUfGFBNEA4KPNmw1jYlDE/1BhOebmONpZQMP/CdcCWL+CKyTdi1/289Ak4B0iSZZN+Yx+AONzPlcvBVi+bXeXtqoIt2zMFKkr/0YFQg= - on: - tags: true - repo: Perryvw/TypescriptToLua diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a50a68074 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug current jest test", + "type": "node", + "request": "launch", + "env": { "CI": "true" }, + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", + "args": ["--runInBand", "--no-cache", "--runTestsByPath", "${relativeFile}"], + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4e33325..235cdc2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,125 +1,945 @@ # Changelog +## 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 +- Support `String(x)` transforming it to `tostring(x)`. +- Fixed statements before class super call +- Fixed some bugs with `LuaMultiReturn` used in iterables + +## 1.27.0 + +- Upgraded TypeScript to 5.6.2 +- Added support for `Math.trunc` (see [Math.trunc()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc)) +- Fixed a runtime error when disposing a disposable class +- Fixed the wrong `this` value being passed when using `?.` to call a `super` method +- Fixed a bug causing exported `/** @compileMembersOnly */` enums to break +- Fixed a bug in `Array.from` when calling it with a non-array iterable +- Fixed an incorrect diagnostic being shown for `await using` code +- Fixed a bug causing not all getters/setters to be transpiled + +## 1.26.0 + +- Upgraded TypeScript to 5.5.2 +- Added support for the new proposed ECMAScript Set methods in ESNext: `intersection`, `union`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`. For more info see [the TypeScript release notes](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#support-for-new-ecmascript-set-methods). +- Fixed a bug causing bundled code to be executed twice in some edge cases. +- Fixed a bug causing errors when using lualib in an environment without coroutines. + +## 1.25.0 + +- Upgraded TypeScript to 5.4.2 +- Added support for new TypeScript 5.4 features `Map.groupBy` and `Object.groupBy` +- Fixed a bug causing files to not be emitted at the correct output path +- Fixed a bug causing `@customname` to not work together with `@noSelf` +- Fixed a bug causing extended tsconfigs to not be correctly read when using watch mode + +## 1.24.0 + +- Optimized promises and async/await to better handle long chains of promises, like for example using await in a loop +- Fixed a bug causing errors when accessing `super` properties + +## 1.23.0 + +- Upgraded TypeScript to 5.3.3 + +## 1.22.0 + +- Added support for `Number.isInteger(n)` +- Added support for `afterEmit` plugin hook that can be used to post-process lua files after (possibly incremental) builds +- Fixed a bug causing `@noSelfInFile` sometimes to be ignored + +## 1.21.0 + +- Added support for `continue` for Lua 5.0, 5.1 and universal targets. +- Added support for the new `/** @customName myCustomName **/` decorator, which allows renaming of variables and identifiers. + - This is useful to get around names that are reserved keywords in TypeScript, but are used in Lua API +- Fixed a bug that caused super calls in static methods to throw an error + +## 1.20.0 + +- Added support for `Number.parseInt` and `Number.parseFloat` (mapped to same implementation as global `parseInt` and `parseFloat`) +- Added implementation for multiple `Number` constants like `Number.EPSILON` +- Added support for `Array.at` +- Fixed a bug when throwing an error object in a Lua environment without `debug` module +- Fixed a bug causing files not to be found when returning an absolute path from a `moduleResolution` plugin + +## 1.19.0 + +- Added support for the new TypeScript 5.2 `using` keyword for explicit resource management. See the [TypeScript release notes](https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management) for more information. +- Added support for the newly introduced 'copying array methods' `toReversed`, `toSorted`, `toSpliced` and `with`. These were also introduced in TypeScript 5.2, see [their release notes](https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#copying-array-methods) for more information. + +## 1.18.0 + +- Upgraded TypeScript to 5.2.2 +- The `noResolvePaths` option now accepts glob paths (for example, 'mydir/hello\*' to not resolve any files in mydir starting with hello). + - This also allows disabling module resolution completely by providing a '\*\*' pattern in your tsconfig.json `noResolvePaths`. + +## 1.17.0 + +- Added the `moduleResolution` plugin, allowing you to provide custom module resolution logic. See [the docs](https://typescripttolua.github.io/docs/api/plugins#moduleresolution) for more info. +- Added `isEmpty` to `LuaTable`, `LuaMap` and `LuaSet` (and their read-only counterparts). This simply to `next(tbl) == nil`, allowing for a simple check to see if a table is empty or not. +- Fixed a bug with synthetic nodes (e.g. created by custom TypeScript transformers) throwing an exception. +- Fixed unnecessary extra unpacking of tables +- Fixed some bugs with new decorators + +## 1.16.0 + +- Upgraded TypeScript to 5.1.3. +- Added support for [TypeScript 5.0 decorators](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators). + - Old-style decorators will still work as long as you have `experimentalDecorators` configured, otherwise the new standard is used. +- Added support for [class static initialization blocks](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks). +- Fixed a bug causing the `tstl` object in tsconfig.json not to be properly extended when extending a tsconfig from node_modules. + +## 1.15.0 + +- Using `extends` in tsconfig.json now also correctly merges settings in the `tstl` block (shallow merge). +- Now avoiding assigning default parameter values if the default value is `nil` (`null` or `undefined`). +- Fixed a bug where indexing a `LuaMultiReturn` value with [0] would still return everything. +- Fixed a bug with nested namespaces causing unexpected nil indexing errors. + +## 1.14.0 + +- **[Breaking]** Upgraded TypeScript to 5.0. +- Added support for `Number.toFixed`. +- Added support for spread expressions with `LuaPairsIterable` and `LuaPairsKeysIterable`. +- Fixed a bug breaking module resolution when using a custom file extension. +- Fixed various exceptions that could happen when trying to translate invalid TS. + +## 1.13.0 + +- Fixed alternate file extensions (other than .lua, if configured) breaking module resolution and emitted require statements. +- Added experimental support for `"luaLibImport": "require-minimal"` configuration option. This will output a lualib bundle containing only the lualib functions used by your code. This might not work if you are including external tstl-generated Lua, for example from a npm package. +- Added support for the "exports" field in package.json. +- Fixed some exceptions resulting from invalid language-extensions use. +- Fixed an exception when using compound assignment (like `+=`) with array length. + +## 1.12.0 + +- Reworked how tstl detects and rewrites `require` statements during dependency resolution. This should reduce the amount of false-positive matches of require statements: require statements in string literals or comments should no longer be detected by tstl. This means require statements in string literals or comments can survive the transpiler without causing a 'could not resolve lua sources' error or getting rewritten into nonsense. +- Now using `math.mod` for Lua 5.0 modulo operations. + +## 1.11.0 + +- **[Breaking]** Upgraded TypeScript to 4.9. +- `--tstlVerbose` now prints more resolver output when failing to resolve Lua sources. +- Fixed a bug breaking default exported classes with unicode names +- Relaxed conditions for the always-true warning to false positives. + +## 1.10.0 + +- **[Breaking]** Upgraded TypeScript to 4.8. +- **[Breaking]** Changed how language-extensions are distributed, you should now put `"types": ["@typescript-to-lua/language-extensions"]` in your tsconfig.json (instead of "typescript-to-lua/..."). +- Added support for **Lua 5.0**, thanks for the effort @YoRyan! +- Added support for TypeScript 4.7 [instantiation expressions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#instantiation-expressions). +- Fixed a bug causing some `require` uses not being recognized my module resolution, leading to missing files in the output. + +## 1.9.0 + +- Added a warning when trying to use a type in a condition that can never be false in Lua, such as numbers or strings. (Only when `strictNullChecks` is enabled.) +- Fixed some missing and misplaced errors when trying to reference LuaTable/LuaMap/LuaSet functions without calling them. +- Fixed a bug in the `get()` type of `ReadOnlyLuaMap`. It is now typed the same as `LuaMap`, i.e. it can return `undefined`. +- Fixed an issue in bundling that could sometimes lead to invalid bundle entry requires. +- Added a warning when using `paths` without specifying `baseUrl`. +- Fixed exception while checking for standard library types. + +## 1.8.0 + +- Added support for the [tsconfig.json paths](https://www.typescriptlang.org/tsconfig#paths) configuration option. +- Fixed spreading lua iterables & iterators translating to incorrect lua. + - You can now write things like `[...pairs(obj)]`. +- Fixed a bug in module resolution resolving the wrong lua files when having the same file names in nested directories. +- Fixed a bug causing temporary variables for nested destructuring in loop variables to be outside the loop, instead of inside. +- Fixed import expressions not actually requiring their import. +- Fixed `super` calls not being source-mapped correctly. + +## 1.7.0 + +- Added support for `LuaMap` and `LuaSet` language extensions that translate to very low level Lua table operations. See [our docs](https://typescripttolua.github.io/docs/advanced/language-extensions/#luamap-and-luaset) for more information. +- Big performance improvements, speeding up TSTL translation by 2-3x. Thanks @GlassBricks! +- Reduced the use of temorary variables. +- Moved tsconfig-schema into main TypeScriptToLua repository. +- Added support for array options in tstl CLI. +- Fixed bug where promise `then` was not correctly forwarding the result value to chained promises. +- Fixed a bug causing false positive errors from jsdoc documentation comments. +- Fixed various calling context bugs. + +## 1.6.0 + +- **[Breaking]** Upgraded TypeScript to 4.7 +- Fixed a bug where EmitOptions plugins were ignored +- Fixed a bug where sometimes function calls (like those to a custom jsx factory) would have a context argument even though `noImplicitSelf` was specified. +- Fixed a bug where sometimes `noImplicitSelf` was ignored because of incorrect file path separators. +- Fixed lualib_bundle files not correctly being included from node_module packages. +- Fixed compound assignment operators (e.g. ??= or ||=) not correctly updating the lhs if used as expression instead of statement. + +## 1.5.0 + +- Added support for `Array.from` and `Array.of` +- Added support for `beforeEmit` hook to plugins that runs after tstl is totally done, but before emitting the result. + - For more info about plugin hooks, see: https://typescripttolua.github.io/docs/api/plugins +- Added support for import expressions (`import("./module").then(m => m.foo());`) +- Added tsconfig setting `lua51AllowTryCatchInAsyncAwait` to disable restrictions on try/catch in combination with async/await in 5.1 (default: false) +- Added tsconfig setting `noImplicitGlobalVariables` to disable tstl making variables global in non-module files. +- Various lualib optimizations +- JSDoc comments from input TS are now also part of output Lua as LDoc comments. + - Can be disabled with `removeComments` tsconfig setting. +- Rewrote how try/catch works in async functions, fixing many bugs. +- Fixed a bug where methods with non-null expressions (i.e. `obj.method!()`) would not pass the correct self parameter, causing runtime errors. +- Fixed a bug where symlinked node_modules (for example when using `npm link`) were not recognized as external dependencies by module resolution. +- Fixed a bug with sourcemap traceback leading to invalid lua +- Improved sourcemap traceback interaction with `loadstring` + +## 1.4.0 + +- Upgraded to TypeScript 4.6 +- Added two event hooks to TSTL plugins: `beforeTransform` and `afterPrint` + - These allow you to run plugin code at specific points in the transpilation process. +- Lualib polyfills are now modules required into locals, instead of global functions + - This change also removes the `"always"` option for the `"lualibImport"` tsconfig key. +- Added support for `Math.sign` +- Switched to `^` instead of `math.pow`, the latter was deprecated in 5.3 +- Added an error when using `null` or `undefined` in tuples, as that is undefined behavior in the Lua spec and causes unexpected behavior +- Added tsconfig setting `extension`, allowing to specify a different output file extension +- Fixed multiple issues with optional chaining and lualib/language extensions +- Fixed issue assigning function with properties to variable declarations +- Fixed multiple issues with preceding statements in class constructors +- Fixed external code module resolution exploding into a stack overflow in complicated module hierarchies +- Fixed a `function.apply(context)` breaking the transpiler if called with only one parameter +- Fixed preceding statements in ternary conditionals (`x ? y : z`) leading to incorrect code + +## 1.3.0 + +- Added `LuaPairsIterable` language extension to mark objects as iterable with Lua's `pairs`. +- Added support for properties on functions. +- Unicode is no longer escaped and used as-is for `"luaTarget": "JIT"`. +- Fixed some bugs related to destructuring in loops and function parameters. +- Fixed incorrect global being generated for some `try` statements. +- Fixed some incorrect behavior for `??` and `? :` when generic types were involved. +- Added missing `...` optimization in cases where casts or parentheses are used. +- Fixed a bug for `Promise` when resolving with another Promise. This also fixes some unexpected behavior with `async` which is built with Promises. +- Fixed `async` functions not aborting after returning from a `catch` block. + +## 1.2.0 + +- Upgraded to TypeScript 4.5.x. +- Improved general output formatting. +- Added support for more complicated (nested) destructuring patterns, also fixing an exception they caused before. +- Fixed incorrect interactions between standard library functionality and optional chaining, e.g. `myArray?.forEach()`. +- Fixed rejected promises sometimes not getting the correct rejection reason. +- Fixed some `delete` behavior that was different in Lua compared to JS. +- Fixed a bug causing exported classes to lose their decorators. +- Fixed plugins checking for ts-node from the wrong location (tsconfig directory), plugins will now check for ts-node relative to the tstl directory. + +Under the hood: + +- We can now transform using preceding statements, allowing all kinds of optimizations and improvements to output Lua. +- Updated various language constructs to use preceding statements instead of inline immediately-invoked functions. + +## 1.1.0 + +- **[Breaking]** We now use TypeScript's JSX transformer instead of maintaining our own. As a result, `React.createElement` now requires a self parameter, so remove `@noSelf`, `this: void` if necessary. +- **[Breaking(-ish)]** Due to limitations in 5.1, try/catch can no longer be used in async or generator functions when targetting Lua 5.1. This was already broken but now tstl will explicitly give an error if you try. +- Added support for the `switch` statement in all versions! (Before they were not supported in 5.1 and universal). +- Added support for `string.prototype.replaceAll` and improved `string.prototype.replace` implementation. +- Added `noResolvePaths` tsconfig option to disable module resolution for environment-provided modules. +- Implemented support for void expressions, i.e `void(0)` or `void(ignoreThisReturnValue())`. +- Upgraded TypeScript to 4.4.4 and made it a peer dependency to hopefully avoid plugin issues due to mismatching TypeScript versions. +- The `$vararg` language extension can be used to access CLI arguments, now also in bundles. +- Fixed a bug regarding `baseUrl` and relative imports. +- Fixed `sourceMapTraceback: true` not working correctly for bundles. +- Fixed an issue regarding hoisting in switch case clauses. +- Added missing function context validation cases for object literals. +- Fixed a problem where awaiting rejected promises in try/catch would give the wrong result. +- Fixed an issue where chained `.then` calls on already-resolved or already-rejected promises would cause some callbacks to not fire while they should. +- Fixed source file paths in source maps being absolute, they are now relative again. + +## 1.0.0 + +- **[Breaking]** `/* @tupleReturn */` has been removed and will no longer have any effect. You will get an error if you try ot use it or if you use declarations that use it. +- Added support for the `Promise` class. +- Added support for `async` and `await` using coroutines. +- Module resolution now also correctly resolves `/init.lua` files for `require("")`. +- Fixed an error not being thrown when trying to call a method in an optional chain that does not exist. (If the method itself is not optional) +- Fixed a bug where parentheses could break the context parameter being resolved for a method. +- Fixed a bug where context parameters in object literal methods were not inferred correctly. +- Fixed a bug with sourceMapTraceback. +- Fixed TS emitting empty JSON files if you use JSON source files. + +## 0.42.0 + +- **[Breaking]** The `/** @tupleReturn */` is now deprecated, and will be removed next release. If you are still using it, please upgrade to the [LuaMultiReturn language extension](https://typescripttolua.github.io/docs/advanced/language-extensions#luamultireturn-type). +- Added support for JSX, see [documentation](https://typescripttolua.github.io/docs/jsx) for more information. +- Added support for the `baseUrl` configuration key for module resolution. + +A large list of bugfixes: + +- Fixed an exception causing tstl to exit when trying to assign to an optional chain. +- Fixed resolved files appearing duplicated in lua bundles. +- Fixed a problem resolving external Lua files in nested directories. +- Fixed `@noResolution` in library packages losing their NoResolution tag, causing attempts to resolve them for package users. +- Fixed a bug in the bundling code causing modules not to be cached if they return nil (which happens if they are not a module) +- Fixed module resolution trying to incorrectly resolve and rewrite things like `myObject.require()` or `my_custom_require()`. +- Fixed lualib bundle not being included in the output if external packages use it, but the client code does not. + +## 0.41.0 + +- Added support for [optional chaining](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html) `a?.b`, `a?.[b]` and `a?.()`. +- Added an error when trying to bundle a library (`"buildmode": "library"`) project. +- Added `--tstlVerbose` CLI flag to help with diagnosing problems. +- Fixed a bug where vararg (`...`) was not correctly optimized. +- Fixed .tsx files not correctly being resolved. +- Fixed a bug where files were emitted to the wrong location if no `outDir` was specified. + +## 0.40.0 + +- Added support for using external Lua code in your project. This means you can create and install node_modules packages containing Lua code. It also lets you include Lua source files as part of your source files. Used Lua will automatically be added to your output. For more information, see the [External Lua Code](https://typescripttolua.github.io/docs/external-lua-code) page in the docs. +- **[Breaking]** Removed support for deprecated annotations that have been replaced with language extensions: `/** @luaIterator */`, `/** @vararg */`, `/** @luatable */` and `/** forRange */`. If you were still using these, see [the docs](https://typescripttolua.github.io/docs/advanced/compiler-annotations) for instructions how to upgrade. +- Added support for `array.entries()`. +- Added support for `LuaTable.has(key)` and `LuaTable.delete(key)` to the language extensions. See [docs](https://typescripttolua.github.io/docs/advanced/language-extensions#lua-table-types) for more info. +- Made language extension types more strict, disallowing `null` and `undefined` in some places where they would cause problems in Lua. + +- Fixed an issue where using TypeScript transformer plugins would cause invalid namespace and module code, as well as breaking hoisting. +- Fixed invalid switch statement output when the `default` clause was not the last clause in the switch. +- Fixed missing LuaLib dependency when using `string.split`. +- Fixed **lots** of bundling bugs and issues, also added the TypeScriptToLua header to the top of the bundle unless _noHeader_ is specified. + +Under the hood: + +- Various improvements to testing infrastructure for testing (virtual) projects with multiple files. + +## 0.39.0 + +- **[Breaking]** Removed support for `@phantom`, `@metaExtension`, `@extension`, `@pureAbstract` compiler annotations. As of this version **these will no longer function!** For help upgrading your code see [the deprecated annotation docs](https://typescripttolua.github.io/docs/advanced/compiler-annotations#deprecated). +- Added official support for **Lua 5.4**. +- Added the `LuaTable` language extension. This allows the use of barebones Lua tables for key-value storage, without suffering from JS's forced string indexes. For more information see [the LuaTable docs](https://typescripttolua.github.io/docs/advanced/language-extensions#lua-table-types). +- Deprecated `@vararg`. Instead, tstl will figure out itself when use of the Lua ellipsis token (`...`) is appropriate. Also language extension `$vararg` was added to force use of the ellipsis token. See [the docs](https://typescripttolua.github.io/docs/advanced/language-extensions#vararg-constant) for more information. +- Added `trailingComments` and `leadingComments` fields to statements in the Lua AST. These can be modified by plugins to emit comments in the output lua. For an example see [the add-comments test plugin](https://github.com/TypeScriptToLua/TypeScriptToLua/blob/master/test/transpile/plugins/add-comments.ts). + +Under the hood: + +- Tests are now run on a WebAssembly-compiled version of official Lua releases. This means we can now execute test Lua on all Lua versions (except LuaJIT). Shoutout to [Fengari](https://github.com/fengari-lua/fengari) for serving us well for a long time. + +## 0.38.0 + +- **[Breaking]** Renamed `MultiReturn` to `LuaMultiReturn` to be consistent with other language extensions all starting with Lua-. +- Fixed various bugs and issues related to `LuaMultiReturn`, including its value not correctly being wrapped in some cases. +- Added support for indexing `LuaMultiReturn` values without destructing. +- Added language extensions to allow translation directly to (overwritten) Lua operators like `+`,`-`,`..`. For more information see [Operator Map Types](https://typescripttolua.github.io/docs/advanced/language-extensions#operator-map-types). +- Added language extension `$range()`. This function can be used to create numeric lua loops, for example `for (const i of $range(1, 10)) {` translates to `for i=1,10 do`. For more information see [\$range Iterator Function](https://typescripttolua.github.io/docs/advanced/language-extensions#range-iterator-function). +- Added support for `Array.isArray`, formalizing tstl's isArray convention (**note:** Due to `[]` and `{}` being the same in Lua, `{}` - without any keys - is considered an array too.) +- Added support for `string.prototype.includes`. +- Added support for enum merging. +- Fixed missing lualib dependency in `string.prototype.split`. +- Added a not-supported diagnostic for not-yet-implemented optional chaining (`a?.b`). +- Moved remaining legacy tests to modern testing utils, removing the legacy testing code. + +## 0.37.0 + +- **[Important]** Deprecated the @phantom, @extension, @metaExtension and @pureAbstract annotations. This is done because there are good alternatives in regular TypeScript, and this helps us simplify the transpiler. For now, using these annotations will result in a warning but they will still continue to function. A few months from now these annotations will no longer be supported, so upgrade if possible. See [Compiler Annotations](https://typescripttolua.github.io/docs/advanced/compiler-annotations) for more info. +- Added the `MultiReturn<>` type and `$multi()` helper function as the first language extensions. This is to provide a type-safe alternative to the `@tupleReturn` annotation. For more information see [the new Language Extensions page](https://typescripttolua.github.io/docs/advanced/language-extensions) on the docs website. +- Removed some class transformation code from the transpiler that was no longer used. +- Fixed a bug causing object spread to malfunction in some cases ([#898](https://github.com/TypeScriptToLua/TypeScriptToLua/issues/898)). +- Omitted `tostring` for parameters of template literals (`` `${}` ``) that are already known strings. +- Fixed a bug causing incorrect Lua syntax to be generated in some cases ([#944](https://github.com/TypeScriptToLua/TypeScriptToLua/issues/944)). + +## 0.36.0 + +- Upgraded to TypeScript 4.0. +- Added support for `parseInt` and `parseFloat`. +- Added support for `yield*` [for generator functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*). +- Added support for [method, property and accessor decorators](https://www.typescriptlang.org/docs/handbook/decorators.html). +- Shebangs at the top of a .ts file will now be preserved. +- Fixed an issue causing declarations referencing their own identifier to cause a nil reference error. + +## 0.35.0 + +- In preparation for some new features, some public APIs have been changed: + - High-level APIs that read input files from the file system (`transpileFiles` and `transpileProject`) now write transpiled files by default. This behavior can be changed by providing a `writeFile` callback, similarly to TypeScript's `program.emit`. + - `transpile` and `emitTranspiledFiles` functions have been replaced with the `Transpiler` class. See [documentation](https://typescripttolua.github.io/docs/api/overview#low-level-api) for usage examples. +- Fixed `declarationDir` option not being respected. +- `Function.length` is supported now. +- String iteration is now supported. +- Exposed `parseConfigFileWithSystem` to parse _tsconfig.json_ files as part of the tstl API. +- Fixed `string.replace` incorrectly escaping some `replaceValue` characters (`().+-*?[^$`) +- Fixed several other string operations behaving differently from JS (mostly regarding indices out of bounds and NaN arguments). +- Fixed a bug where the length argument of `String.prototype.substr` was evaluated twice. +- Fixed some missing dependencies in LuaLib classes (Map, Set, WeakMap, WeakSet) + +## 0.34.0 + +- Added new `"luaTarget"` option value - `"universal"`. Choosing this target makes TypeScriptToLua generate code compatible with all supported Lua targets. + + - **BREAKING CHANGE:** This is a new default target. If you have been depending on LuaJIT being chosen implicitly, you now have to enable it explicitly with `"luaTarget": "JIT"` in the `tsconfig.json` file. + +- TypeScript has been updated to **3.9**. See [release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html) for details. This update includes some fixes specific to our API usage: + + - Importing a non-module using `import "./file"` produced a TS2307 error [#35973](https://github.com/microsoft/TypeScript/issues/35973) + - TypeScript now tries to find a call signature even in presence of type errors [#36665](https://github.com/microsoft/TypeScript/pull/36665): + ```ts + function foo(this: void, x: string) {} + foo(1); + ``` + ```lua + -- Before: with 3.8 (this: void ignored due to signature mismatch) + foo(nil, 1) + -- Now: with 3.9 + foo(1) + ``` + +- Reduced memory consumption and optimized performance of generators and iterators +- Fixed generator syntax being ignored on methods (`*foo() {}`) and function expressions (`function*() {}`) +- Fixed iteration over generators stopping at first yielded `nil` value +- Fixed `Array.prototype.join` throwing an error when array contains anything other than strings and numbers +- Fixed extending a class not keeping `toString` implementation from a super class + +- Fixed issue where CLI arguments were incorrectly removed. +- Fixed issue where class accessors threw an error due to a missing dependency. + +Under the hood: + +- Upgraded to Prettier 2.0 + +## 0.33.0 + +- Added support for nullish coalescing `A ?? B`. +- Annotation `/** @noSelf */` now also works directly on function declarations, not only on classes/interfaces. +- Fixed incorrect file paths in source maps. +- Fixed unknown node kind throwing an error instead of diagnostic. +- Fixed string index with side-effects being evaluated twice. +- Added check for node.js version when running tstl. +- Fixed some issues with reflection class names. + +- Fixed incorrectly escaped variable names. + +Under the hood: + +- Switched from TSLint to ESLint. +- Added benchmarking capability for garbage collection. + +## 0.32.0 + +- **Deprecated:** The `noHoisting` option has been removed, hoisting will always be done. + +- TypeScript has been updated to 3.8. See [release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html) for details. + +- Fixed class accessors not working when base class is lacking type information (#725) + +- Class extension code has been extracted to lualib + + ```ts + class A {} + class B extends A {} + ``` + + ```diff + A = __TS__Class() + B = __TS__Class() + -B.____super = A + -setmetatable(B, B.____super) + -setmetatable(B.prototype, B.____super.prototype) + +__TS__ClassExtends(A, B) + ``` + +- Generated code for class accessors is more dynamic now + + ```ts + class A { + get a() { + return true; + } + } + ``` + + ```diff + A = __TS__Class() + -A.prototype.____getters = {} + -A.prototype.__index = __TS__Index(A.prototype) + -function A.prototype.____getters.a(self) + - return true + -end + +__TS__SetDescriptor( + + A.prototype, + + "a", + + { + + get = function(self) + + return true + + end + + } + +) + ``` + + This change simplifies our codebase and opens a path to object accessors implementation + +- Errors reported during transpilation now are created as TypeScript diagnostics instead of being thrown as JavaScript errors. This makes TypeScriptToLua always try to generate valid code (even in presence of errors) and allows multiple errors to be reported in a single file: + + ```ts + for (var x in []) { + } + ``` + + ```shell + $ tstl file.ts + file.ts:1:1 - error TSTL: Iterating over arrays with 'for ... in' is not allowed. + file.ts:1:6 - error TSTL: `var` declarations are not supported. Use `let` or `const` instead. + + $ cat file.lua + for x in pairs({}) do + end + ``` + +- Added `tstl.luaPlugins` option, allowing to specify plugins in a `tsconfig.json` file: + + ```json + { + "tstl": { + "luaPlugins": [{ "name": "./plugin.ts" }] + } + } + ``` + +- Added support for all valid TS `for ... of` loop variable patterns. + +- Fixed a bug where spread expressions in array literals were not correctly translated: + + ```diff + - [1, ...[2, 3], 4] // --> { 1, 2, 4 } + + [1, ...[2, 3], 4] // --> { 1, 2, 3, 4 } + + - ((...values) => values)(1, ...[2, 3], 4) // --> { 1, 2, 4 } + + ((...values) => values)(1, ...[2, 3], 4) // --> { 1, 2, 3, 4 } + ``` + +- Fixed Lua error when left hand side of `instanceof` was not a table type. + +- Fixed `sourcemapTraceback` function returning a value different from the standard Lua result in 5.1. + +- Fixed missing LuaLib dependency for Error LuaLib function. + +- Fixed several issues with exported identifiers breaking `for ... in` loops and some default class code. + +- Fixed overflowing numbers transforming to undefined Infinity, instead they are now transformed to `math.huge`. + +## 0.31.0 + +- **Breaking:** The old annotation syntax (`/* !varArg */`) **no longer works**, the only currently supported syntax is: + + `/** @varArg */`. + +- **Breaking:** Fixed some cases where variables were **incorrectly** not labeled `local`. The only variables that are implicitly put in the global context are _top-level variables in non-module files, without any imports or exports in their file_. + +- Moved handling of parentheses out of the transformers and unified this logic in the printer. This might result in some more parentheses in the generated code, but also makes it more correct and fixes some related bugs. + +- Added support for `array.includes`. + +- Fixed a bug breaking global augmentation. + +- Fixed hoisting breaking if there were synthetic nodes in the AST (i.e. when a TS transformer modified the AST). + +## 0.30.0 + +- **Breaking:** We dropped support for `var` variables. If you still have any `var` variable declarations, please use `let` or `const` instead. +- **Breaking:** We now depend on Node.js >= 12.13.0 +- Added support for string `trimLeft`, `trimRight`, `trimStart` and `trimEnd`. +- Added support for `console.error`, `console.warn` and `console.info` , they will all be transpiled to Lua's `print`. +- Avoided exporting anonymous identifiers. +- Fixed an issue when assigning to an already-exported variable. +- Math.atan2 will now be transpiled to the correct Lua atan2 (or atan for 5.3) method. +- Fixed various destructuring issues. +- Fixed incorrect error for intersection types containing built-ins (like `number` or `string`) +- Modules containing `import` or `export` will now always be recognized as module to match TypeScript's logic. +- Fixed `true` not being recognized as lua keyword. +- Fixed inaccuracies in switch case variable scoping. +- Fixed various problems with variables being global instead of local. + +### Internal: + +- Refactored transformation pipeline from one big LuaTransformer class to many small modules. +- Moved class construction methods from transformer to LuaLib. +- Upgraded dependencies. + +## 0.29.0 + +- Added bundling support using options `luaBundle` and `luaBundleEntry` (so **not** TS's outFile). This will bundle all output files into one single bundle file, with _luaBundleEntry_ as entry point. For more information on these options see https://github.com/TypeScriptToLua/TypeScriptToLua/wiki#tstl-specific-options +- Added support for `Number.prototype.toString(radix)`. +- Fixed `array.flat()` not flattening empty arrays. + **Note:** Due to language restrictions, flat will also flatten _objects_ without keys (or only undefined values) so be careful. + For more info on this issue see https://github.com/TypeScriptToLua/TypeScriptToLua/pull/737 +- Fixed runtime error when throwing non-string errors and `sourceMapTraceback`. + +## 0.28.0 + +- We now have a `noImplicitSelf` option you can add to your tstl tsconfig.json. Default behavior is `false`. Setting this option to `true` will cause no 'self' arguments to be considered/generated in the project. Declarations will behave as if they have a `/** @noSelfInFile */` directive. This option is new and might cause correctness issues, use at your own risk and create an issue if you experience any issues. +- Regular `Error` objects can now be thrown, `throw` is no longer limited to only strings. Take care: printing/toString'ing the LuaLib error class might have different results for different Lua versions. +- Added LuaLib support for `array.reduceRight`. +- Added LuaLib support for `array.find`. + +- Fixed an issue in test code causing some inconsistent behavior between JS <-> Lua to go unnoticed. Also fixed `array.splice` and `array.join` whose Lua versions behaved differently from the ECMAScript specification. +- Fixed array.reduce not behaving according to ECMAScript specification. +- Fixed order of operations issue with ternary conditional. + +- Updated to TS 3.6. +- Moved from Travis+Appveyor to GitHub Actions! + +## 0.27.0 + +- Added support for [array and object destructuring with rest](https://basarat.gitbooks.io/typescript/content/docs/destructuring.html#object-destructuring-with-rest). +- Changed Map and Set implementations to they preserve insertion order when iterated over, as specified by ECMAScript. + +- Fixed an issue with [`/** @luaTable */`](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/Compiler-Directives#luaTable) variable names disappearing. +- Fixed for-in loops throwing an error when using a pre-defined variable. +- Fixed issue with initialization order of class properties. + +- Simplified enum transformation code. + +## 0.26.0 + +- Added support for [default exports and export equals statements](https://github.com/Microsoft/TypeScript/issues/7185#issuecomment-421632656). +- Added support for [object spread expressions](https://mariusschulz.com/blog/object-rest-and-spread-in-typescript). +- Added support for most common [destructuring assignments](https://basarat.gitbooks.io/typescript/content/docs/destructuring.html). +- Added support for omitted declarations in destructuring tuples. (i.e. `const [a,,c] = foo();`) + +- `@noSelf` now only applies to members of the namespace with the directive, in case of namespace merging. +- Fixed issue with isNumerType causing enum members as array indices not to recieve the `+1`. +- Fixed string.indexOf failing in case the search string was a Lua string pattern. +- Fixed some crashes from recursive type constraints. + +- Some simplification to the printing of expression statements. +- Added new testing util methods to improve the testing process. + +## 0.25.0 + +- Added support for named function assignments, i.e. `const myFunc = function x(n) { ...; return x(n - 1); }` + +- Made detection of string methods more robust. +- Fixed issue regarding readonly tuple detection. +- Fixed a nasty issue causing exponential complexity on chained properties/method expressions. +- Improved handling of constrained generic types related to string and array detection. + +## 0.24.0 + +- Returns in try/catch statements now properly return from the current function. +- TypeScript's `globalThis` is now translated to lua's `_G`. Lualib functions were updated where relevant. + +- Fixed issue where string/table literals were missing parentheses and caused lua syntax errors. +- Various improvements/refactorings across the codebase. +- Fixed syntax error in for...of loops with empty destructuring argument. +- Fixed issue with `do ... while` scope. +- Fixed a bug with [@combileMembersOnly](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/Compiler-Directives#compilemembersonly) where it would ignore anything before the enum name. + +## 0.23.0 + +- Added support for OmittedExpression in array literals and array binding patterns. +- Added support for [tagged template literals](https://basarat.gitbooks.io/typescript/docs/template-strings.html#tagged-templates). +- Changed output lua formatting to be more debugger-friendly. +- Various improvements to source maps. + +- Fixed an issue with the interaction of super calls and exported classes. +- Fixed `@noResolution` not working on named modules. +- Fixed namespace merging not working due to an earlier change. + +- Some refactoring and plumbing for the website. + +## 0.22.0 + +- Added the [@vararg](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/Compiler-Directives#vararg) directive. +- Added the [@forRange](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/Compiler-Directives#forRange) directive. +- Custom ts transformers can now be loaded from tsconfig. + +- Fixed default tstl header incorrectly showing up above lualib functions. +- Some improvements to typeof expressions. + +## 0.21.0 + +- Imports/exports that are ambient (declarations, types, interfaces, etc) or are not used in value positions no longer generate `require` statements. +- For ... of loops are now translated using `ipairs`. +- Added support for `array.reduce`. +- Added support for `import foo = bar.baz;` statements. + +- Fixed some issues with binding pattern parameter default values. +- Fixed some issues with variable naming. +- Enabled prettier on the entire codebase. + +## 0.20.0 + +- Added support for `string.repeat`, `string.padStart` and `string.padEnd`. +- Added automatic variable renaming for invalid Lua identifiers. +- Fixed `/** @tupleReturn */` not working for function types (i.e `myFunc: () => [number, number]`) +- Various improvements to source map output format. +- Various small code tweaks and improvements. + +## 0.19.0 + +- **BREAKING CHANGE:** All tstl-specific options should now be inside the "tstl" section in tsconfig.json (see README.md). **Root-level options are no longer supported**. +- Added a compiler API to programmatically invoke TypeScriptToLua, and to modify or extend the default transpiler. More info on the [Compiler API wiki page](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/TypeScriptToLua-API). +- Added support for [class decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators). +- Added support for the [@luaTable directive](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/Compiler-Directives#luatable) which will force a class to be transpiled as vanilla lua table. +- Added support for NaN, Infinity and related number functions. +- Added support for string.startsWith, string.endsWith and improved string.replace implementation. +- Added support for Symbol.hasInstance. + +- Hoisting now also considers imports. +- Various improvements to iterators and arrays, they also work with the spread operator now. +- Fixed an issue with parameters that had `false` as default value. + +## 0.18.0 + +- Added support for setting array length. Doing `array.length = x` will set the length of the array to `x` (or shorter, if the starting array was shorter!). +- Added the `.name` property to all transpiled classes, so `class.name` will contain the classname as string. +- Changed `class = class or {}` syntax to just be `class = {}`. +- Cleaned up printer output so it produces more human-readable code. + +- Fixed bug with expression statements. +- Fixed incorrect inline sourcemap format. +- Fixed bug when merging an interface and module. +- Fixed a bug with inherited constructor super call ordering. + +- Enabled strict tsconfig. + +## 0.17.0 + +- We now support source maps in the [standard JS v3 format](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1). You can generate source maps with the `--sourceMap` CLI argument, or by adding `sourceMap: true` to your tsconfig. Inline source maps are also supported with `--inlineSourceMap` CLI/tsconfig parameter. +- Also added [tstl option](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki#tstl-specific-options) `--sourceMapTraceback`, which will add an override to Lua's `debug.traceback()` to each file, so source maps will automatically be applied to Lua stacktraces (i.e. in errors). + +- Made watch mode incremental. + +- Added support for `Object.fromEntries`, `array.flat` and `array.flatMap`. + +- **BREAKING CHANGE:** Directive `@tupleReturn` should now be specified **per overload**. + +- Fixed a bug where rest parameters would not transpile correctly. +- Fixed an issue with escaped backticks. +- Various small fixes function inference and array detection. + +- Changed testing framework to [jest](https://github.com/facebook/jest). + +## 0.16.0 + +- **BREAKING CHANGE:** All functions now take a `self` parameter. This means that without further action calls to declaration functions might be given an extra argument. + - To remove the self parameter from a single function add `this: void` to its declaration: + `declare function foo(this: void, ...)` + - To remove the self parameter from all methods or functions in a class/interface/namespace add `/** @noSelf */`: + `/** @noSelf */ interface Foo {` + - To remove the self parameter from all functions in a file, add `/** @noSelfInFile */` at the top. + +--- + +- **BREAKING CHANGE:** Directive `/** @luaIterator */` should now be put on types instead of on the functions returning them. + +--- + +- Fixed a bug breaking named class expressions. +- Fixed inconsistency between the meaning of `>>` and `>>>` in JS vs. Lua. +- Added `/** @noResolution */` directive to prevent path resolution on declared modules. +- It is now possible to put `/** @luaIterator */` on types extending `Array`. +- Fixed issue with the moment static fields were initialized. +- Fixed issue where `undefined` as property name was not transpiled correctly. +- Various improvements to function/method self parameter inference. +- Tstl options can now be defined in their own `tstl` block in tsconfig.json. For example: + +``` +{ + "compilerOptions" : {} + "tstl": { + "luaTarget": "JIT" + } +} +``` + +- Fixed issue when redeclaring TypeScript libraries/globals. +- Fixed exception resolving function signatures. +- Added support for automatically transpiling several `console` calls to their Lua equivalent: + - `console.log(...)` -> `print(...)` + - `console.assert(...)` -> `assert(...)` + - `console.trace(...)` -> `print(debug.traceback(...))` +- Added support for `array.findIndex()`. +- Fixed `array.sort()` not working with a compare function. +- Added support for several common `Math.` functions and constants. +- Added support for several common string instance functions such as `upper()`. + +## 0.15.2 + +- Several improvements to module path resolution. +- Removed header comment appearing in lualib. +- Several package config improvements. +- Static get/set accessors. + +## 0.15.1 + +- Fixed array detection for unit and intersection types. +- Support for import without `from`. +- Added support for `WeakMap` and `WeakSet`. +- Added support for `Object.keys` and `Object.assign`. +- Added support for importing JSON files. +- Fixed bug with where loop variables were not properly scoped. +- Added support for ExportDeclarations + ## 0.15.0 -* Now written for TypeScript 3.3.x! -* Removed external CLI parser dependency and wrote our own `CommandLineParser.ts` to read CLI and tsconfig input. -* Added support for hoisting, can be disabled with the `noHoisting` option in CLI or tsconfig. -* Added support for generator functions. -* Reworked classes into a system more similar to JavaScript with prototype tables. -* Improved support for ObjectBindingPatterns. -* Added support for enums with identifier values. -* Added support for the binary comma operator. -* Added support for `string.concat`, `string.slice` and `string.charCodeAt`. -* Refactored LuaTranspiler.emitLuaLib to its own method so it can be called from external code. -* Improved function type inference. -* Fixed some bugs in for loops with expressions. -* Fixed a bug forwarding luaIterator functions. + +- Now written for TypeScript 3.3.x! +- Removed external CLI parser dependency and wrote our own `CommandLineParser.ts` to read CLI and tsconfig input. +- Added support for hoisting, can be disabled with the `noHoisting` option in CLI or tsconfig. +- Added support for generator functions. +- Reworked classes into a system more similar to JavaScript with prototype tables. +- Improved support for ObjectBindingPatterns. +- Added support for enums with identifier values. +- Added support for the binary comma operator. +- Added support for `string.concat`, `string.slice` and `string.charCodeAt`. +- Refactored LuaTranspiler.emitLuaLib to its own method so it can be called from external code. +- Improved function type inference. +- Fixed some bugs in for loops with expressions. +- Fixed a bug forwarding luaIterator functions. ## 0.14.0 -* Reworked internal transpiler structure to be more suited for future extension. -* Reworked module and exports system. -* Added support for custom iterators. -* Improved formatting consistency. -* Errors are now reported with location `(line, column)` instead of `line: line, column: column`. -* Added back default lua header: `--[[ Generated with https://github.com/Perryvw/TypescriptToLua ]]`. -* Fixed some bugs with switches and breaks. -* Fixed several bugs with functions and context parameters. + +- Reworked internal transpiler structure to be more suited for future extension. +- Reworked module and exports system. +- Added support for custom iterators. +- Improved formatting consistency. +- Errors are now reported with location `(line, column)` instead of `line: line, column: column`. +- Added back default lua header: `--[[ Generated with https://github.com/Perryvw/TypescriptToLua ]]`. +- Fixed some bugs with switches and breaks. +- Fixed several bugs with functions and context parameters. ## 0.13.0 -* Reworked how functions are transpiled, see https://github.com/Perryvw/TypescriptToLua/wiki/Differences-Between-Functions-and-Methods -* Improved handling of types extending Array. -* Fixed several bugs with classes. -* Fixed issues with inherited accessors. + +- Reworked how functions are transpiled, see https://github.com/TypeScriptToLua/TypescriptToLua/wiki/Differences-Between-Functions-and-Methods +- Improved handling of types extending Array. +- Fixed several bugs with classes. +- Fixed issues with inherited accessors. ## 0.12.0 -* Added detection of types extending Array. -* Added new JSDoc-style compiler directives, deprecated the old `!` decorators, see https://github.com/Perryvw/TypescriptToLua/wiki/Compiler-Directives -* Fixed bug with constructor default values. -* The Lualib is no longer included when not used. -* Fixed bug with unpack in LuaJIT. + +- Added detection of types extending Array. +- Added new JSDoc-style compiler directives, deprecated the old `!` decorators, see https://github.com/TypeScriptToLua/TypescriptToLua/wiki/Compiler-Directives +- Fixed bug with constructor default values. +- The Lualib is no longer included when not used. +- Fixed bug with unpack in LuaJIT. ## 0.11.0 -* Fixed bug when throwing anything that was not a string. (@tomblind) -* Added support for object literal method declarations. (@tomblind) -* Fixed several issues with assignment operators (@tomblind) -* `else if` statements are now transpiled to Lua `elseif` instead of nested ifs statements. (@tomblind) -* Occurrences of const enum values are now directly replaced with their value in the Lua output. (@DoctorGester) -* Rethrowing is now possible from try/catch blocks (@tomblind) -* Destructing statements in LuaJit now use `unpack` instead of `table.unpack` -* Removed support for switch statements for versions <= 5.1. -* Refactored `for ... of` translation, it now uses numeric `for ` loops instead of `ipairs` for performance reasons. + +- Fixed bug when throwing anything that was not a string. (@tomblind) +- Added support for object literal method declarations. (@tomblind) +- Fixed several issues with assignment operators (@tomblind) +- `else if` statements are now transpiled to Lua `elseif` instead of nested ifs statements. (@tomblind) +- Occurrences of const enum values are now directly replaced with their value in the Lua output. (@DoctorGester) +- Rethrowing is now possible from try/catch blocks (@tomblind) +- Destructing statements in LuaJit now use `unpack` instead of `table.unpack` +- Removed support for switch statements for versions <= 5.1. +- Refactored `for ... of` translation, it now uses numeric `for` loops instead of `ipairs` for performance reasons. ## 0.10.0 -* Added support for NonNullExpression (`abc!` transforming the type from `abc | undefined` to `abc`) -* Added expression position to replacement binary expression to improve error messages. -* Fixed various issues with !TupleReturn (@tomblind) -* Added support for `array.reverse`, `array.shift`, `array.unshift`, `array.sort`. (@andreiradu) -* Added translation for `Object.hasOwnProperty()`. (@andreiradu) -* Added support for class expressions (@andreiradu) -* Fixed bug in detecting array types (@tomblind) -* Added public API functions and better webpack functionality. + +- Added support for NonNullExpression (`abc!` transforming the type from `abc | undefined` to `abc`) +- Added expression position to replacement binary expression to improve error messages. +- Fixed various issues with !TupleReturn (@tomblind) +- Added support for `array.reverse`, `array.shift`, `array.unshift`, `array.sort`. (@andreiradu) +- Added translation for `Object.hasOwnProperty()`. (@andreiradu) +- Added support for class expressions (@andreiradu) +- Fixed bug in detecting array types (@tomblind) +- Added public API functions and better webpack functionality. ## 0.9.0 -* Fixed an issue where default parameter values were ignored in function declarations. -* Fixed a bug where `self` was undefined in function properties. -* Fixed a bug where addition of +1 to indices sometimes caused issues with operation order (thanks @brianhang) -* Fixed super calls having issues with their `self` instance. (thanks @hazzard993) -* Methods now also accept custom decorators (thanks @hazzard993) -* Improved support for `toString` calls (thanks @andreiradu) -* Added support for block expressions (thanks @andreiradu) + +- Fixed an issue where default parameter values were ignored in function declarations. +- Fixed a bug where `self` was undefined in function properties. +- Fixed a bug where addition of +1 to indices sometimes caused issues with operation order (thanks @brianhang) +- Fixed super calls having issues with their `self` instance. (thanks @hazzard993) +- Methods now also accept custom decorators (thanks @hazzard993) +- Improved support for `toString` calls (thanks @andreiradu) +- Added support for block expressions (thanks @andreiradu) Thanks @tomblind for the following changes: -* Fixed a bug where recursive use of a function expression caused a nil error. -* Fixed syntax error when compiling variable declaration lists. -* Fixed an issue with assignment order in exported namespaces. -* Various fixes to `!TupleReturn` functions. -* Fixed an issue with declaration merging. + +- Fixed a bug where recursive use of a function expression caused a nil error. +- Fixed syntax error when compiling variable declaration lists. +- Fixed an issue with assignment order in exported namespaces. +- Various fixes to `!TupleReturn` functions. +- Fixed an issue with declaration merging. ## 0.8.0 -* Added experimental watch mode, use it with `tstl --watch` -* Refactored decorators -* Added `...` spread operator -* Added error when a lua keyword is used as variable name -* Added support for shorthand object literals (thanks @gakada) -* Added array.pop (thanks @andreiradu) -* Added `;` after lines to avoid ambiguous syntax (thanks @andreiradu) -* Fixed issue with tsconfig being overriden (thanks @Janne252) + +- Added experimental watch mode, use it with `tstl --watch` +- Refactored decorators +- Added `...` spread operator +- Added error when a lua keyword is used as variable name +- Added support for shorthand object literals (thanks @gakada) +- Added array.pop (thanks @andreiradu) +- Added `;` after lines to avoid ambiguous syntax (thanks @andreiradu) +- Fixed issue with tsconfig being overriden (thanks @Janne252) ## 0.7.0 -* Lualib runtime library is now compiled from TypeScript using the transpiler when building! - * Split up runtime library definition into individual files. - * Added multiple inclusion modes using the tsconfig option `lubLibImport`, options are: - * `require` : Requires the entire library if lualib features are used. - * `always` : Always require the runtime library. - * `inline` : Inline the library code for used features in the file. - * `none` : Do not include the runtime library -* Added support for assigning expressions (`+=`, `&=`, `++`, etc) in other expressions (i.e. `lastIndex = i++` or `return a += b`) by transpiling them as immediately called anonymous functions. -* Unreachable code (after returns) is no longer transpiled, preventing a Lua syntax error. -* Fixed issue with destructing statements in Lua 5.1 -* Fixed issue with escaped characters in strings. -* Fixed bug regarding changing an exported variable after its export. +- Lualib runtime library is now compiled from TypeScript using the transpiler when building! + - Split up runtime library definition into individual files. + - Added multiple inclusion modes using the tsconfig option `lubLibImport`, options are: + - `require` : Requires the entire library if lualib features are used. + - `always` : Always require the runtime library. + - `inline` : Inline the library code for used features in the file. + - `none` : Do not include the runtime library +- Added support for assigning expressions (`+=`, `&=`, `++`, etc) in other expressions (i.e. `lastIndex = i++` or `return a += b`) by transpiling them as immediately called anonymous functions. +- Unreachable code (after returns) is no longer transpiled, preventing a Lua syntax error. +- Fixed issue with destructing statements in Lua 5.1 +- Fixed issue with escaped characters in strings. +- Fixed bug regarding changing an exported variable after its export. ## 0.6.0 -* Reworked part of the class system to solve some issues. -* Reworked class tests from translation to functional. -* Fixed issue with Lua splice implementation. -* Added threaded test runner to use for faster testing (use with `npm run test-threaded`). -* Added support for string-valued enums. -* Added tsconfig values to target Lua 5.1 and 5.2. + +- Reworked part of the class system to solve some issues. +- Reworked class tests from translation to functional. +- Fixed issue with Lua splice implementation. +- Added threaded test runner to use for faster testing (use with `npm run test-threaded`). +- Added support for string-valued enums. +- Added tsconfig values to target Lua 5.1 and 5.2. ## 0.5.0 -* Added support for `**` operator. -* Added support for `~` operator. -* Improved handling of assignment binary operators (`+=`,`*=`,`&=`, etc). -* Rewrote `Map` and `Set` to implement the ES6 specification for [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). -* Added support for `baseUrl` in [tsconfig](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). -* Added `bit32` bit operations for Lua 5.2. -* Fixed various little bugs. -* Added tslint rule to enforce use of `/** @override */` decorator. -* Improved tests. + +- Added support for `**` operator. +- Added support for `~` operator. +- Improved handling of assignment binary operators (`+=`,`*=`,`&=`, etc). +- Rewrote `Map` and `Set` to implement the ES6 specification for [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) and [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). +- Added support for `baseUrl` in [tsconfig](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). +- Added `bit32` bit operations for Lua 5.2. +- Fixed various little bugs. +- Added tslint rule to enforce use of `/** @override */` decorator. +- Improved tests. ## 0.4.0 -* Added support for `typeof` -* Added support for `instanceof` -* Added support for [TypeScript overloads](https://www.typescriptlang.org/docs/handbook/functions.html#overloads) + +- Added support for `typeof` +- Added support for `instanceof` +- Added support for [TypeScript overloads](https://www.typescriptlang.org/docs/handbook/functions.html#overloads) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13f2db018..f44bb3522 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,80 +1,65 @@ # Contributing to TypeScriptToLua -1) [Project Overview](#project-overview) -2) [Running Tests](#running-tests) -3) [Testing Guidelines](#testing-guidelines) -4) [Coding Conventions](#coding-conventions) +1. [Project Overview](#project-overview) +2. [Running Tests](#running-tests) +3. [Testing Guidelines](#testing-guidelines) +4. [Coding Conventions](#coding-conventions) ## Project Overview + To get familiar with the project structure, here is a short overview of each directory and their function. + - `src/` - * Source code for the project, has the transpiler core files in its root. - * `src/lualib/` + - Source code for the project, has the transpiler core files in its root. + - `src/lualib/` - Contains the TypeScript source for the lualib. This consists of implementations of standard TypeScript functions that are not present in Lua. These files are compiled to Lua using the transpiler. They are included in the Lua result when transpiling. - * `src/targets/` + - `src/targets/` - Version-specific transpiler overrides for the different Lua targets. The main transpiler transpiles Lua 5.0, each target-specific transpiler extends the transpiler of the version before it, so the 5.3 inherits 5.2 which inherits 5.1 which inherits 5.0. LuaJIT is based on 5.2 so inherits from the 5.2 transpiler. - * *Compiler.ts* - Main entry point of the transpiler, this is what interfaces with the TypeScript compiler API. - * *Transpiler.ts* - Main transpiler code, transforms a TypeScript AST to a Lua string. - * *TSHelper.ts* - Helper methods used during the transpilation process. + - _Compiler.ts_ - Main entry point of the transpiler, this is what interfaces with the TypeScript compiler API. + - _LuaTransformer.ts_ - Main transpiler code, transforms a TypeScript AST to a Lua AST. + - _LuaPrinter.ts_ - Transforms a Lua AST to a string. + - _TSHelper.ts_ - Helper methods used during the transpilation process. - `test/` - * This directory contains all testing code for the transpiler. - * `test/src/` - - Contains all extra source and utility used to run tests. - * `test/unit/` + - This directory contains all testing code for the transpiler. + - `test/unit/` - Unit/Functional tests for the transpiler. Tests in here are grouped by functionality they are testing. Generally each of these tests uses the transpiler to transpile some TypeScript to Lua, then executes it using the Fengari Lua VM. Assertion is done on the result of the lua code. - * `test/translation/` + - `test/translation/` - **[Obsolete]** Contains tests that only check the transpiled Lua String. We prefer adding unit/functional tests over translation tests. This directory will probably be removed at some point. ## Running Tests -The tests for this project can be executed using the standard `npm test`. This runs all tests (can take a while!). -### Testing while developing -Due to the time required to run all tests, it is impractical to run every test while developing part of the transpiler. To speed up the test run you can import `FocusTest` or `FocusTests` from Alsatian. If a class is decorated with `@FocusTests`, all other test classes will be ignored. Similarly, if any test method is decorated with `@FocusTest`, only `@FocusTest` methods will be run during `npm test`. +The tests for this project can be executed using the standard `npm test`. This runs all tests. -For example: -```ts -import { Expect, FocusTests, Test, TestCase } from "alsatian"; +Due to the time required to run all tests, it is impractical to run every test while developing part of the transpiler. To speed up the test run you can: -@FocusTests -export class FunctionTests { - // All tests in here will be executed. -} +- Use `npm test name` to run tests that match a file name pattern -// All other tests will be ignored. -``` +- Use `npm test -- --watch [name]` to start tests and rerun them on change -Or +- Check out `Watch Usage` in the watching interface to get information about filtering tests without restarting CLI -```ts -import { Expect, FocusTest, Test, TestCase } from "alsatian"; +- Use `.only` and `.skip` to filter executed tests in the file -export class FunctionTests { - @FocusTest - @Test("Example test 1") - public test1(): void { // Will be executed - } - - @FocusTest - @Test("Example test 2") - public test2(): void { // Will also be executed - } - - @Test("Example test 3") - public test3(): void { // Will be ignored - } -} - -// All other tests will be ignored. -``` + ```ts + // Skipped + test("test", () => {}); + // Executed + test.only("test", () => {}); + ``` ## Testing Guidelines + When submitting a pull request with new functionality, we require some functional (transpile and execute Lua) to be added, to ensure the new functionality works as expected, and will continue to work that way. Translation tests are discouraged as in most cases as we do not really care about the exact Lua output, as long as executing it results in the correct result (which is tested by functional tests). ## Coding Conventions -Most coding conventions are enforced by the ts-lint configuration. The test process will fail if code does not pass the linter. Some extra conventions worth mentioning: -* Do not abbreviate variable names. The exception here are inline lambda arguments, if it is obvious what the argument is you can abbreviate to the first letter, e.g: `statements.filter(s => ts.VariableStatement(s))` -* Readability of code is more important than the amount of space it takes. If extra line breaks make your code more readable, add them. -* Functional style is encouraged! + +Most coding conventions are enforced by the TSLint and Prettier. You can check your code locally by running `npm run lint`. The CI build will fail if your code does not pass the linter. For better experience, you can install extensions for your code editor for [TSLint](https://palantir.github.io/tslint/usage/third-party-tools/) and [Prettier](https://prettier.io/docs/en/editors.html). + +Some extra conventions worth mentioning: + +- Do not abbreviate variable names. The exception here are inline lambda arguments, if it is obvious what the argument is you can abbreviate to the first letter, e.g: `statements.filter(s => ts.VariableStatement(s))` +- Readability of code is more important than the amount of space it takes. If extra line breaks make your code more readable, add them. +- Functional style is encouraged! diff --git a/LICENSE b/LICENSE index 4ae072ee1..5c655aca8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [fullname] +Copyright (c) TypeScriptToLua Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 256287258..13198efc1 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,44 @@ -# TypeScriptToLua -A generic TypeScript to Lua transpiler. Write your code in TypeScript and publish Lua! - -Large projects written in lua can become hard to maintain and make it easy to make mistakes. Writing code in TypeScript instead improves maintainability, readability and robustness, with the added bonus of good IDE support. This project is useful in any environment where Lua code is accepted, with the powerful option of simply declaring any existing API using TypeScript declaration files. - -[![Build Status](https://travis-ci.org/Perryvw/TypescriptToLua.svg?branch=master)](https://travis-ci.org/Perryvw/TypescriptToLua) -[![Build status](https://ci.appveyor.com/api/projects/status/github/perryvw/typescripttolua?branch=master&svg=true)](https://ci.appveyor.com/project/Perryvw/typescripttolua) -[![Coverage](https://codecov.io/gh/perryvw/typescripttolua/branch/master/graph/badge.svg)](https://codecov.io/gh/perryvw/typescripttolua) - -You can chat with us on [Discord: ![Discord](https://img.shields.io/discord/515854149821267971.svg)](https://discord.gg/BWAq58Y) - -## Documentation -More detailed documentation and info on writing declarations can be found [on the wiki](https://github.com/Perryvw/TypescriptToLua/wiki). - -Changelog can be found in [CHANGELOG.md](https://github.com/Perryvw/TypescriptToLua/blob/master/CHANGELOG.md) +
+ TypeScriptToLua +

+

TypeScriptToLua

+ CI status + Coverage + Chat with us! +

+ Documentation + | + Try Online + | + Changelog + | + Contribution guidelines +
+ +--- -## Usage Guide - -**Install** - -`npm install -g typescript-to-lua` - -**Compile Files** - -`tstl path/to/file.ts path/to/other-file.ts` +A generic TypeScript to Lua transpiler. Write your code in TypeScript and publish Lua! -**Compile Projects** +Large projects written in Lua can become hard to maintain and make it easy to make mistakes. Writing code in TypeScript instead improves maintainability, readability and robustness, with the added bonus of good [tooling] support (including [ESLint], [Prettier], [Visual Studio Code] and [WebStorm]). This project is useful in any environment where Lua code is accepted, with the powerful option of simply declaring any existing API using TypeScript declaration files. -`tstl -p path/to/tsconfig.json` +[tooling]: https://typescripttolua.github.io/docs/editor-support +[eslint]: https://eslint.org/ +[prettier]: https://prettier.io/ +[visual studio code]: https://code.visualstudio.com/ +[webstorm]: https://www.jetbrains.com/webstorm/ -**Compile project in watch mode** +## Getting Started -`tstl -p path/to/tsconfig.json --watch` +To install TypeScriptToLua add the `typescript-to-lua` npm package: -**Example tsconfig.json** +```bash +$ npm install -D typescript-to-lua ``` -{ - "compilerOptions": { - "noImplicitAny" : true, - "noImplicitThis" : true, - "alwaysStrict" : true, - "strictNullChecks": true - }, - "luaTarget": "JIT" -} -``` - -## Contributing -All contributions are welcome, but please read our [contribution guidelines](https://github.com/Perryvw/TypescriptToLua/blob/master/CONTRIBUTING.md)! - -## Declarations -The real power of this transpiler is usage together with good declarations for the Lua API provided. Some examples of Lua interface declarations can be found here: -- [Dota 2 Modding](https://github.com/ModDota/API/tree/master/declarations/server) -- [Defold Game Engine Scripting](https://github.com/dasannikov/DefoldTypeScript/blob/master/defold.d.ts) -- [LÖVE 2D Game Development](https://github.com/hazzard993/love-typescript-definitions) - -## Building & Tests - -`npm run build` to build the project. - -`npm run test` to run tests. -`npm run test-threaded` runs test in parallel, faster but less detailed output. - -`npm run coverage` or `npm run coverage-html` to generate a coverage report. - -## Sublime Text integration -This compiler works great in combination with the [Sublime Text Typescript plugin](https://github.com/Microsoft/TypeScript-Sublime-Plugin) (available through the package manager as `TypeScript`). - -You can simply open your typescript project assuming a valid tsconfig.json file is present. The default TypeScript plugin will provide all functionality of a regular TypeScript project. - -### Setting up a custom build system -To add the option to build with the Lua transpiler instead of the regular typescript compiler, go to `Tools > Build System > New Build System...`. In the new sublime-build file that opens, enter the following (adjust path to tstl if not installed globally): +This package includes the `tstl` command line application, which can be used similarly to `tsc`: ``` -{ - "cmd": ["tstl", "-p", "$file"], - "shell": true -} +$ npx tstl ``` -Save this in your Sublime settings as a `TypeScriptToLua.sublime-build`. You can now select the TypeScriptToLua build system in `Tools > Build System` to build using the normal hotkey (`ctrl+B`), or if you have multiple TypeScript projects open, you can choose your compiler before building by pressing `ctrl+shift+B`. + +For more information, check out [Getting Started](https://typescripttolua.github.io/docs/getting-started) in our documentation. diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 513dd7593..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Test against the latest version of this Node.js version -environment: - nodejs_version: "8" - -# Cache dependencies -cache: - - node_modules - -# Install scripts. (runs after repo cloning) -install: - # Get the latest stable version of Node.js or io.js - - ps: Install-Product node $env:nodejs_version - # install modules - - npm install - -# Post-install test scripts. -test_script: - # Output useful info for debugging. - - node --version - - npm --version - # run tests - - npm run build - - npm test - -# Don't actually build. -build: off diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 000000000..321218c9c --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,47 @@ +## TSTL Benchmarks + +These benchmarks are written in typescript and transpiled to lua by using tstl. + +### Memory benchmarks + +To add a new benchmark add a new file to `memory_benchmarks` +and **default** export a function with the following type: `() => void`. +To prevent the benchmark from reporting "useful" results of your benchmark function as garbage, simply return the result. +The memory used by the returned result won't count towards the total garbage amount. + +For example (memory_benchmarks/my_benchmark.ts): + +```ts +export default function myBenchmark() { + const n = 123; + const result = []; + for (let i = 0; i < n; i++) { + // Do something memory instensive + } + return result; // Return results so they wont be counted as garbage +} +``` + +**Goal** + +The goal of memory benchmarks is to track how much (memory) `"garbage"` is created by tstl. +For that reason garbage collection is disabled in the benchmarks. + +You can force the creation of `"garbage"` by creating a lot of anonymous functions or temporary tables (see [lua-users.org](http://lua-users.org/wiki/OptimisingGarbageCollection) for more information). + +To avoid crashes in the CI your benchmark should not use more than 500MB of memory. + +### Runtime benchmarks + +To add a new runtime benchmark (execution time), add a new file to `runtime_benchmarks` and **default** export a function with the following type: `() => void`. + +### Running locally + +1. Create a benchmark baseline called "benchmark_baseline.json": + `tstl -p tsconfig.53.json && cd dist && lua -- run.lua benchmark_baseline.json` +2. Make some changes to tstl. +3. Create an updated benchmark and compare with the baseline: + `tstl -p tsconfig.53.json && cd dist && lua -- run.lua benchmark_updated.json benchmark_baseline.json` +4. The above command will output comparison data as json to stdout. + If you provide a path as third argument the comparison data will be written to that path instead. + `tstl -p tsconfig.53.json && cd dist && lua -- run.lua benchmark_updated.json benchmark_baseline.json result.md` diff --git a/benchmark/src/benchmark_types.ts b/benchmark/src/benchmark_types.ts new file mode 100644 index 000000000..38c2c1fd6 --- /dev/null +++ b/benchmark/src/benchmark_types.ts @@ -0,0 +1,38 @@ +export enum BenchmarkKind { + Memory = "memory", + Runtime = "runtime", +} + +export type BenchmarkFunction = () => void; + +export type BenchmarkResult = MemoryBenchmarkResult | RuntimeBenchmarkResult; + +export enum MemoryBenchmarkCategory { + TotalMemory = "totalMemory", + Garbage = "garbage", +} + +export interface MemoryBenchmarkResult { + kind: BenchmarkKind.Memory; + categories: Record; + benchmarkName: string; +} + +export function isMemoryBenchmarkResult(result: BenchmarkResult): result is MemoryBenchmarkResult { + return result.kind === BenchmarkKind.Memory; +} + +export interface RuntimeBenchmarkResult { + kind: BenchmarkKind.Runtime; + time: number; + benchmarkName: string; +} + +export function isRuntimeBenchmarkResult(result: BenchmarkResult): result is RuntimeBenchmarkResult { + return result.kind === BenchmarkKind.Runtime; +} + +export interface ComparisonInfo { + summary: string; + text: string; +} diff --git a/benchmark/src/benchmark_util.ts b/benchmark/src/benchmark_util.ts new file mode 100644 index 000000000..fedbfdb93 --- /dev/null +++ b/benchmark/src/benchmark_util.ts @@ -0,0 +1,60 @@ +import { BenchmarkResult } from "./benchmark_types"; +import { calculatePercentageChange, toFixed } from "./util"; + +export const makeMarkdownTableRow = (cells: string[]) => `| ${cells.join(" | ")} |\n`; +export const makeBold = (input: string) => `**${input}**`; + +export function compareNumericBenchmarks( + newResults: T[], + oldResults: T[], + unit: string, + extractValue: (result: T) => number, + formatValue: (value: number) => string +): string { + let comparisonTable = makeMarkdownTableRow([ + "name", + `master (${unit})`, + `commit (${unit})`, + `change (${unit})`, + "change (%)", + ]); + comparisonTable += makeMarkdownTableRow(["---", "---", "---", "---", "---"]); + + let oldValueSum = 0; + let newValueSum = 0; + + newResults.forEach(newResult => { + const oldResult = oldResults.find(r => r.benchmarkName === newResult.benchmarkName); + const newValue = extractValue(newResult); + if (oldResult) { + const oldValue = extractValue(oldResult); + const percentageChange = calculatePercentageChange(oldValue, newValue); + const change = newValue - oldValue; + const row = [ + newResult.benchmarkName, + formatValue(oldValue), + formatValue(newValue), + formatValue(change), + toFixed(percentageChange, 2), + ]; + comparisonTable += makeMarkdownTableRow(row); + oldValueSum += oldValue; + newValueSum += newValue; + } else { + // No master found => new benchmark + const row = [newResult.benchmarkName, formatValue(newValue), "/", "/", "/"]; + comparisonTable += makeMarkdownTableRow(row); + } + }); + + const sumPercentageChange = calculatePercentageChange(oldValueSum, newValueSum); + comparisonTable += makeMarkdownTableRow([ + makeBold("sum"), + makeBold(formatValue(oldValueSum)), + makeBold(formatValue(newValueSum)), + makeBold(formatValue(newValueSum - oldValueSum)), + makeBold(toFixed(sumPercentageChange, 2)), + ]); + + return comparisonTable; +} diff --git a/benchmark/src/json.lua b/benchmark/src/json.lua new file mode 100644 index 000000000..54d444840 --- /dev/null +++ b/benchmark/src/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/benchmark/src/memory_benchmark.ts b/benchmark/src/memory_benchmark.ts new file mode 100644 index 000000000..8434f3881 --- /dev/null +++ b/benchmark/src/memory_benchmark.ts @@ -0,0 +1,71 @@ +import { BenchmarkKind, MemoryBenchmarkResult, ComparisonInfo, MemoryBenchmarkCategory } from "./benchmark_types"; +import { compareNumericBenchmarks } from "./benchmark_util"; +import { toFixed, json } from "./util"; + +export function runMemoryBenchmark(benchmarkFunction: () => void): MemoryBenchmarkResult { + const result: MemoryBenchmarkResult = { + kind: BenchmarkKind.Memory, + benchmarkName: "NO_NAME", + categories: { + [MemoryBenchmarkCategory.Garbage]: 0, + [MemoryBenchmarkCategory.TotalMemory]: 0, + }, + }; + + // collect before running benchmark + collectgarbage("collect"); + + // stop automatic gc + collectgarbage("stop"); + + const preExecMemoryUsage = collectgarbage("count"); + + // store return value this allows benchmark functions + // to prevent "useful" result data from being garbage collected + let temp = benchmarkFunction(); + + const postExecMemoryUsage = collectgarbage("count"); + + collectgarbage("restart"); + collectgarbage("collect"); + + // get the amount of garbage collected + result.categories[MemoryBenchmarkCategory.Garbage] = postExecMemoryUsage - collectgarbage("count"); + + // make sure result isn't garbage collected until now and supress unused var warning + temp = temp; + + result.benchmarkName = debug.getinfo(benchmarkFunction).short_src; + + result.categories[MemoryBenchmarkCategory.TotalMemory] = postExecMemoryUsage - preExecMemoryUsage; + + return result; +} + +const formatMemory = (memInKB: number) => toFixed(memInKB / 1024, 3); + +export function compareMemoryBenchmarks( + oldResults: MemoryBenchmarkResult[], + newResults: MemoryBenchmarkResult[] +): ComparisonInfo { + // Can not use Object.values because we want a fixed order. + const categories = [MemoryBenchmarkCategory.TotalMemory, MemoryBenchmarkCategory.Garbage]; + + const summary = categories + .map(category => `### ${category}\n\n${compareCategory(newResults, oldResults, category)}`) + .join("\n"); + + const text = `### master:\n\`\`\`json\n${json.encode(oldResults)}\n\`\`\`\n### commit:\n\`\`\`json\n${json.encode( + newResults + )}\n\`\`\``; + + return { summary, text }; +} + +function compareCategory( + newResults: MemoryBenchmarkResult[], + oldResults: MemoryBenchmarkResult[], + category: MemoryBenchmarkCategory +): string { + return compareNumericBenchmarks(newResults, oldResults, "mb", result => result.categories[category], formatMemory); +} diff --git a/benchmark/src/memory_benchmarks/array_concat.ts b/benchmark/src/memory_benchmarks/array_concat.ts new file mode 100644 index 000000000..fbe149703 --- /dev/null +++ b/benchmark/src/memory_benchmarks/array_concat.ts @@ -0,0 +1,9 @@ +export default function arrayConcat(): number[] { + let arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const n = 1000; + for (let i = 0; i < n; i++) { + arr1 = arr1.concat(arr2); + } + return arr1; +} diff --git a/benchmark/src/memory_benchmarks/array_every.ts b/benchmark/src/memory_benchmarks/array_every.ts new file mode 100644 index 000000000..0ba21334f --- /dev/null +++ b/benchmark/src/memory_benchmarks/array_every.ts @@ -0,0 +1,9 @@ +export default function arrayEvery(): boolean { + const arr = []; + const n = 10000; + for (let i = 0; i < n; i++) { + arr[i] = i; + } + const isSmallerN = arr.every(e => e < n); + return isSmallerN; +} diff --git a/benchmark/src/memory_benchmarks/array_push.ts b/benchmark/src/memory_benchmarks/array_push.ts new file mode 100644 index 000000000..15c0a7dd1 --- /dev/null +++ b/benchmark/src/memory_benchmarks/array_push.ts @@ -0,0 +1,8 @@ +export default function arrayPush(): number[] { + const n = 10000; + const numberList: number[] = []; + for (let i = 0; i < n; i++) { + numberList.push(i * i); + } + return numberList; +} diff --git a/benchmark/src/memory_benchmarks/class_creation.ts b/benchmark/src/memory_benchmarks/class_creation.ts new file mode 100644 index 000000000..aaa57742f --- /dev/null +++ b/benchmark/src/memory_benchmarks/class_creation.ts @@ -0,0 +1,18 @@ +class A { + constructor(x: number) { + this.z = x * x; + } + + public z: number; +} + +class B extends A {} + +export default function classCreation(): B[] { + const arr1 = []; + const n = 10000; + for (let i = 0; i < n; i++) { + arr1.push(new B(i)); + } + return arr1; +} diff --git a/benchmark/src/memory_benchmarks/graph_cylce.ts b/benchmark/src/memory_benchmarks/graph_cylce.ts new file mode 100644 index 000000000..c75372d07 --- /dev/null +++ b/benchmark/src/memory_benchmarks/graph_cylce.ts @@ -0,0 +1,52 @@ +type Graph = Map; + +function range(start: number, end: number): number[] { + if (start > end) return []; + return [start, ...range(start + 1, end)]; +} + +export default function detectCyleBenchmark(): boolean { + const n = 500; + const benchmarkGraph = new Map(); + // build a graph with n nodes and no cycle + for (let i = 0; i < n; i++) { + benchmarkGraph.set(i, range(i, n - 1)); + } + return detectCycle(benchmarkGraph); +} + +/** + * Detects cycles in an undirected graph + */ +function detectCycle(graph: Graph): boolean { + const visited: Map = new Map(); + + return [...graph.keys()].some(current => { + if (!visited.get(current)) { + return _detectCycle(graph, current, visited, undefined); + } + }); +} + +function _detectCycle(graph: Graph, current: T, visited: Map, parent: T | undefined): boolean { + visited.set(current, true); + + const neighbours = graph.get(current); + + if (!neighbours) { + throw Error("Err invalid graph format"); + } + + return neighbours.some(neighbour => { + if (!visited.get(neighbour)) { + // If an adjacent is not visited, then recur for that adjacent + return _detectCycle(graph, neighbour, visited, current); + } else if (neighbour !== parent) { + /* + * If an adjacent node is visited and not a parent of current vertex, + * then there is a cycle. + */ + return true; + } + }); +} diff --git a/benchmark/src/run.ts b/benchmark/src/run.ts new file mode 100644 index 000000000..8b02cc7da --- /dev/null +++ b/benchmark/src/run.ts @@ -0,0 +1,105 @@ +import { runMemoryBenchmark, compareMemoryBenchmarks } from "./memory_benchmark"; +import { + isMemoryBenchmarkResult, + BenchmarkResult, + MemoryBenchmarkResult, + ComparisonInfo, + RuntimeBenchmarkResult, + isRuntimeBenchmarkResult, + BenchmarkKind, +} from "./benchmark_types"; +import { runRuntimeBenchmark, compareRuntimeBenchmarks } from "./runtime_benchmark"; +import { json, loadBenchmarksFromDirectory, readFile } from "./util"; + +// CLI arguments +// arg[0]: output path for benchmark data +// arg[1]: path to baseline benchmark data (required to generate comparison) +// arg[2]: path to result markdown file (optional) +declare const arg: [string | undefined, string | undefined, string | undefined]; + +function benchmark(): void { + // Memory tests + + const memoryBenchmarks = loadBenchmarksFromDirectory("memory_benchmarks"); + const memoryBenchmarkNewResults: MemoryBenchmarkResult[] = memoryBenchmarks.map(runMemoryBenchmark); + + // Run time tests + + const runtimeBenchmarks = loadBenchmarksFromDirectory("runtime_benchmarks"); + const runtimeBenchmarkNewResults: RuntimeBenchmarkResult[] = runtimeBenchmarks.map(runRuntimeBenchmark); + + const newBenchmarkResults = [...memoryBenchmarkNewResults, ...runtimeBenchmarkNewResults]; + + // Try to read the baseline benchmark result + let oldBenchmarkResults: BenchmarkResult[] = []; + if (arg[1]) { + const oldBenchmarkData = readFile(arg[1]); + oldBenchmarkResults = json.decode(oldBenchmarkData) as BenchmarkResult[]; + } + + // Output comparison info + oldBenchmarkResults.sort(sortByName); + newBenchmarkResults.sort(sortByName); + outputBenchmarkData(oldBenchmarkResults, newBenchmarkResults); +} +benchmark(); + +function sortByName({ benchmarkName: a }: BenchmarkResult, { benchmarkName: b }: BenchmarkResult): number { + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +function compareBenchmarks( + oldResults: BenchmarkResult[], + newResults: BenchmarkResult[] +): Record { + const oldResultsMemory = oldResults.filter(isMemoryBenchmarkResult); + const newResultsMemory = newResults.filter(isMemoryBenchmarkResult); + + const memoryComparisonInfo = compareMemoryBenchmarks(oldResultsMemory, newResultsMemory); + + const oldResultsRuntime = oldResults.filter(isRuntimeBenchmarkResult); + const newResultsRuntime = newResults.filter(isRuntimeBenchmarkResult); + + const runtimeComparisonInfo = compareRuntimeBenchmarks(oldResultsRuntime, newResultsRuntime); + + return { + [BenchmarkKind.Memory]: memoryComparisonInfo, + [BenchmarkKind.Runtime]: runtimeComparisonInfo, + }; +} + +function formatComparisonMarkdownFile(comparisonInfo: Record): string { + let result = ""; + const benchmarkKinds = [BenchmarkKind.Memory, BenchmarkKind.Runtime]; + for (const kind of benchmarkKinds) { + result += comparisonInfo[kind].summary + "\n"; + } + for (const kind of benchmarkKinds) { + result += comparisonInfo[kind].text + "\n"; + } + return result; +} + +function outputBenchmarkData(oldResults: BenchmarkResult[], newResults: BenchmarkResult[]): void { + // Output benchmark results to json + if (arg[0]) { + if (arg[1]) { + // if baseline is provide output full comparison info + const comparisonInfo = compareBenchmarks(oldResults, newResults); + const jsonDataFile = io.open(arg[0], "w+")[0]!; + jsonDataFile.write(json.encode({ old: oldResults, new: newResults, comparison: comparisonInfo })); + } else { + const jsonDataFile = io.open(arg[0], "w+")[0]!; + jsonDataFile.write(json.encode(newResults)); + } + } + if (arg[2]) { + // Output to file as markdown if arg[2] is set, this is useful for local development + // Compare results + const comparisonInfo = compareBenchmarks(oldResults, newResults); + const markdownDataFile = io.open(arg[2], "w+")[0]!; + markdownDataFile.write(formatComparisonMarkdownFile(comparisonInfo)); + } +} diff --git a/benchmark/src/runtime_benchmark.ts b/benchmark/src/runtime_benchmark.ts new file mode 100644 index 000000000..c56cd1b0d --- /dev/null +++ b/benchmark/src/runtime_benchmark.ts @@ -0,0 +1,43 @@ +import { BenchmarkKind, ComparisonInfo, RuntimeBenchmarkResult } from "./benchmark_types"; +import { compareNumericBenchmarks } from "./benchmark_util"; +import { json, toFixed } from "./util"; + +export function runRuntimeBenchmark(benchmarkFunction: () => void): RuntimeBenchmarkResult { + const result: RuntimeBenchmarkResult = { + kind: BenchmarkKind.Runtime, + benchmarkName: "NO_NAME", + time: 0, + }; + + // normalize times a bit + collectgarbage("collect"); + + const startTime = os.clock(); + benchmarkFunction(); + const time = os.clock() - startTime; + + result.benchmarkName = debug.getinfo(benchmarkFunction).short_src; + result.time = time; + return result; +} + +export function compareRuntimeBenchmarks( + oldResults: RuntimeBenchmarkResult[], + newResults: RuntimeBenchmarkResult[] +): ComparisonInfo { + const summary = + "### runtime\n\n" + + compareNumericBenchmarks( + newResults, + oldResults, + "s", + result => result.time, + time => toFixed(time, 4) + ); + + const text = `### master\n\`\`\`json\n${json.encode(oldResults)}\n\`\`\`\n### commit\n\`\`\`json\n${json.encode( + newResults + )}\n\`\`\``; + + return { summary, text }; +} diff --git a/benchmark/src/runtime_benchmarks/array_concat_array.ts b/benchmark/src/runtime_benchmarks/array_concat_array.ts new file mode 100644 index 000000000..9c9a7e83d --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_concat_array.ts @@ -0,0 +1,9 @@ +export default function arrayConcat(): number[] { + const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const n = 50000; + for (let i = 0; i < n; i++) { + arr1.concat(arr2); + } + return arr1; +} diff --git a/benchmark/src/runtime_benchmarks/array_concat_spread.ts b/benchmark/src/runtime_benchmarks/array_concat_spread.ts new file mode 100644 index 000000000..a04eff958 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_concat_spread.ts @@ -0,0 +1,9 @@ +export default function arrayConcat(): number[] { + const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + const n = 50000; + for (let i = 0; i < n; i++) { + arr1.concat(...arr2); + } + return arr1; +} diff --git a/benchmark/src/runtime_benchmarks/array_every.ts b/benchmark/src/runtime_benchmarks/array_every.ts new file mode 100644 index 000000000..f5dbfc4e4 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_every.ts @@ -0,0 +1,7 @@ +export default function arrayEvery() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.every((item, index) => item > index); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_filter.ts b/benchmark/src/runtime_benchmarks/array_filter.ts new file mode 100644 index 000000000..127d2476d --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_filter.ts @@ -0,0 +1,7 @@ +export default function arrayFilter() { + const n = 100000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.filter((item, index) => item > index); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_find.ts b/benchmark/src/runtime_benchmarks/array_find.ts new file mode 100644 index 000000000..8a84ff4e5 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_find.ts @@ -0,0 +1,9 @@ +export default function arrayFind() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.find(value => value === j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_findIndex.ts b/benchmark/src/runtime_benchmarks/array_findIndex.ts new file mode 100644 index 000000000..c058a99df --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_findIndex.ts @@ -0,0 +1,9 @@ +export default function arrayFindIndex() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.findIndex(value => value === j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_flat.ts b/benchmark/src/runtime_benchmarks/array_flat.ts new file mode 100644 index 000000000..36ace2d95 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_flat.ts @@ -0,0 +1,7 @@ +export default function arrayFlat() { + const n = 50000; + const array = [1, 2, [3, [4, 5], 6], 7, [8, 9], 10]; + for (let i = 0; i < n; i++) { + array.flat(2); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_flatMap.ts b/benchmark/src/runtime_benchmarks/array_flatMap.ts new file mode 100644 index 000000000..26c6490e5 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_flatMap.ts @@ -0,0 +1,13 @@ +export default function arrayFlatMap() { + const n = 50000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + array.flatMap((el, index) => { + if (index < 5) { + return [el, el + 1]; + } else { + return el + 2; + } + }); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_foreach.ts b/benchmark/src/runtime_benchmarks/array_foreach.ts new file mode 100644 index 000000000..8e14c8119 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_foreach.ts @@ -0,0 +1,10 @@ +export default function arrayForeach() { + const n = 200000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + array.forEach(value => { + let foo = value * 2; + foo = foo; + }); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_includes.ts b/benchmark/src/runtime_benchmarks/array_includes.ts new file mode 100644 index 000000000..828c167a3 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_includes.ts @@ -0,0 +1,9 @@ +export default function arrayIncludes() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.includes(j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_indexOf.ts b/benchmark/src/runtime_benchmarks/array_indexOf.ts new file mode 100644 index 000000000..333c67988 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_indexOf.ts @@ -0,0 +1,9 @@ +export default function arrayIndexOf() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.indexOf(j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_join.ts b/benchmark/src/runtime_benchmarks/array_join.ts new file mode 100644 index 000000000..5a9f5bae4 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_join.ts @@ -0,0 +1,8 @@ +const array = [1, 2, "3", 4, 3, "6", { foo: 3 }, 8, 9, 10]; + +export default function arrayJoin() { + const n = 3000; + for (let i = 0; i < n; i++) { + array.join("|"); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_map.ts b/benchmark/src/runtime_benchmarks/array_map.ts new file mode 100644 index 000000000..79d00d66a --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_map.ts @@ -0,0 +1,7 @@ +export default function arrayMap() { + const n = 100000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + array.map(value => value * 2); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_push_array.ts b/benchmark/src/runtime_benchmarks/array_push_array.ts new file mode 100644 index 000000000..fcc239a31 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_push_array.ts @@ -0,0 +1,9 @@ +export default function arrayPush(): number[] { + const n = 200000; + const numberList: number[] = []; + const numbers = [1, 2, 3]; + for (let i = 0; i < n; i++) { + numberList.push(...numbers); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/array_push_multiple.ts b/benchmark/src/runtime_benchmarks/array_push_multiple.ts new file mode 100644 index 000000000..dbef5460c --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_push_multiple.ts @@ -0,0 +1,8 @@ +export default function arrayPush(): number[] { + const n = 200000; + const numberList: number[] = []; + for (let i = 0; i < n; i++) { + numberList.push(i * i, i + 1); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/array_push_single.ts b/benchmark/src/runtime_benchmarks/array_push_single.ts new file mode 100644 index 000000000..f09bc21ec --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_push_single.ts @@ -0,0 +1,8 @@ +export default function arrayPush(): number[] { + const n = 500000; + const numberList: number[] = []; + for (let i = 0; i < n; i++) { + numberList.push(i * i); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/array_push_vararg.ts b/benchmark/src/runtime_benchmarks/array_push_vararg.ts new file mode 100644 index 000000000..e780461ee --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_push_vararg.ts @@ -0,0 +1,12 @@ +export default function arrayPush(): number[] { + const n = 200000; + const numberList: number[] = []; + function pushArgs(...args: number[]): void { + numberList.push(...args); + } + + for (let i = 0; i < n; i++) { + pushArgs(3, 4); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/array_reduce.ts b/benchmark/src/runtime_benchmarks/array_reduce.ts new file mode 100644 index 000000000..2d615e80f --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_reduce.ts @@ -0,0 +1,7 @@ +export default function arrayReduce() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.reduce((prev, cur, i) => prev + cur + i, 1); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_reduceRight.ts b/benchmark/src/runtime_benchmarks/array_reduceRight.ts new file mode 100644 index 000000000..9dd97d3d2 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_reduceRight.ts @@ -0,0 +1,7 @@ +export default function arrayReduce() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.reduceRight((prev, cur, i) => prev + cur + i, 1); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_reverse.ts b/benchmark/src/runtime_benchmarks/array_reverse.ts new file mode 100644 index 000000000..539d26138 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_reverse.ts @@ -0,0 +1,7 @@ +export default function arrayReverse(): void { + const n = 500000; + const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + numbers.reverse(); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_slice.ts b/benchmark/src/runtime_benchmarks/array_slice.ts new file mode 100644 index 000000000..5419cb0d9 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_slice.ts @@ -0,0 +1,9 @@ +export default function arraySlice() { + const n = 50000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 6; j++) { + array.slice(j, -j); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_some.ts b/benchmark/src/runtime_benchmarks/array_some.ts new file mode 100644 index 000000000..8add42a56 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_some.ts @@ -0,0 +1,7 @@ +export default function arraySome() { + const n = 200000; + const array = [1, 2, 3, 4, 5, 6, 7, 8, 7, 10]; + for (let i = 0; i < n; i++) { + array.some((item, index) => item < index); + } +} diff --git a/benchmark/src/runtime_benchmarks/array_splice.ts b/benchmark/src/runtime_benchmarks/array_splice.ts new file mode 100644 index 000000000..4620bad86 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_splice.ts @@ -0,0 +1,9 @@ +export default function arraySplice() { + const n = 20000; + const array = [1, 2, 3, 4, 3, 6, 7, 8, 9, 10]; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 10; j++) { + array.splice(j, 2, 1, 2); + } + } +} diff --git a/benchmark/src/runtime_benchmarks/array_unshift.ts b/benchmark/src/runtime_benchmarks/array_unshift.ts new file mode 100644 index 000000000..b0f59a0b2 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/array_unshift.ts @@ -0,0 +1,9 @@ +export default function arrayUnshift(): number[] { + const n = 2000; + const numberList: number[] = []; + const numbers = [1, 2, 3]; + for (let i = 0; i < n; i++) { + numberList.unshift(...numbers); + } + return numberList; +} diff --git a/benchmark/src/runtime_benchmarks/string_concat.ts b/benchmark/src/runtime_benchmarks/string_concat.ts new file mode 100644 index 000000000..47d520e9a --- /dev/null +++ b/benchmark/src/runtime_benchmarks/string_concat.ts @@ -0,0 +1,8 @@ +export default function stringReplace() { + const str = "one"; + const n = 50000; + for (let i = 0; i < n; i++) { + str.concat("two", "three", "four", "five"); + } + return str; +} diff --git a/benchmark/src/runtime_benchmarks/string_replace.ts b/benchmark/src/runtime_benchmarks/string_replace.ts new file mode 100644 index 000000000..e7a00fc35 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/string_replace.ts @@ -0,0 +1,8 @@ +export default function stringReplace() { + const str = "Hello, World!"; + const n = 50000; + for (let i = 0; i < n; i++) { + str.replace("World", "Universe"); + } + return str; +} diff --git a/benchmark/src/runtime_benchmarks/string_replaceAll.ts b/benchmark/src/runtime_benchmarks/string_replaceAll.ts new file mode 100644 index 000000000..268bb3642 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/string_replaceAll.ts @@ -0,0 +1,8 @@ +export default function stringReplaceAll() { + const str = "hello, hello world!"; + const n = 50000; + for (let i = 0; i < n; i++) { + str.replaceAll("Hello", "Goodbye"); + } + return str; +} diff --git a/benchmark/src/runtime_benchmarks/string_split.ts b/benchmark/src/runtime_benchmarks/string_split.ts new file mode 100644 index 000000000..22f6c9bb1 --- /dev/null +++ b/benchmark/src/runtime_benchmarks/string_split.ts @@ -0,0 +1,8 @@ +export default function stringReplaceAll() { + const str = "This is a string"; + const n = 50000; + for (let i = 0; i < n; i++) { + str.split(" "); + } + return str; +} diff --git a/benchmark/src/util.ts b/benchmark/src/util.ts new file mode 100644 index 000000000..41f2159f5 --- /dev/null +++ b/benchmark/src/util.ts @@ -0,0 +1,76 @@ +import { BenchmarkFunction } from "./benchmark_types"; + +export function toFixed(num: number, decimalPlaces = 0): string { + return string.format(`%.${decimalPlaces}f`, num); +} + +export function calculatePercentageChange(oldValue: number, newValue: number): number { + return (newValue / oldValue) * 100 - 100; +} + +// @ts-ignore +export const isWindows = package.config.startsWith("\\"); + +export const json: { + decode: (this: void, str: string) => Record | unknown[]; + encode: (this: void, val: any) => string; +} = require("json"); + +export function readFile(path: string): string { + const [fileHandle] = io.open(path, "rb"); + + if (!fileHandle) { + throw Error(`Can't open file ${path}`); + } + + const fileContent = readAll(fileHandle); + fileHandle.close(); + + return fileContent; +} + +export function readAll(file: LuaFile): string { + const content = file.read(_VERSION === "Lua 5.3" ? "a" : ("*a" as any)); + + if (content) { + return content as string; + } + throw Error(`Can't readAll for file ${file}`); +} + +export function readDir(dir: string): string[] { + const [findHandle] = io.popen(isWindows ? `dir /A-D /B ${dir}` : `find '${dir}' -maxdepth 1 -type f`); + + if (!findHandle) { + throw new Error(`Failed to read dir ${dir}`); + } + + const findResult = readAll(findHandle); + + if (!findHandle.close()) { + throw Error(`readDir popen failed for dir ${dir} see stdout for more information.`); + } + + let files = findResult.split("\n"); + if (isWindows) { + // on windows we need to append the directory path + // on unix this is done by find automatically + files = files.map(f => `${dir}/${f}`); + } else { + // strip leading "./" on unix + files = files.map(f => (f.startsWith(".") && f[1] === "/" ? f.substr(2) : f)); + } + return files.filter(p => p !== ""); +} + +export function loadBenchmarksFromDirectory(benchmarkDir: string): BenchmarkFunction[] { + const benchmarkFiles = readDir(benchmarkDir); + + return benchmarkFiles.map(f => { + // replace slashes with dots + let dotPath = string.gsub(f, "%/", ".")[0]; + // remove extension + dotPath = string.gsub(dotPath, ".lua", "")[0]; + return require(dotPath).default as BenchmarkFunction; + }); +} diff --git a/benchmark/tsconfig.53.json b/benchmark/tsconfig.53.json new file mode 100644 index 000000000..6dbbe3bca --- /dev/null +++ b/benchmark/tsconfig.53.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["lua-types/5.3", "@typescript-to-lua/language-extensions"] + }, + "tstl": { + "luaTarget": "5.3" + } +} diff --git a/benchmark/tsconfig.jit.json b/benchmark/tsconfig.jit.json new file mode 100644 index 000000000..0c6912d31 --- /dev/null +++ b/benchmark/tsconfig.jit.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["lua-types/jit", "@typescript-to-lua/language-extensions"] + }, + "tstl": { + "luaTarget": "JIT" + } +} diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json new file mode 100644 index 000000000..e1c9c2941 --- /dev/null +++ b/benchmark/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + // Dev types are JIT + "types": ["lua-types/jit", "@typescript-to-lua/language-extensions"], + "moduleResolution": "node", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/build_lualib.ts b/build_lualib.ts deleted file mode 100644 index 7ca05f26f..000000000 --- a/build_lualib.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as fs from "fs"; -import * as glob from "glob"; -import {compile} from "./src/Compiler"; -import {LuaLib as luaLib, LuaLibFeature} from "./src/LuaLib"; - -const bundlePath = "./dist/lualib/lualib_bundle.lua"; - -compile([ - "--luaLibImport", - "none", - "--luaTarget", - "5.1", - "--noHeader", - "--outDir", - "./dist/lualib", - "--rootDir", - "./src/lualib", - "--noHeader", - "true", - ...glob.sync("./src/lualib/*.ts"), -]); - -if (fs.existsSync(bundlePath)) { - fs.unlinkSync(bundlePath); -} - -const bundle = luaLib.loadFeatures(Object.keys(LuaLibFeature).map(lib => LuaLibFeature[lib])); -fs.writeFileSync(bundlePath, bundle); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..0ea4d78ca --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,230 @@ +// @ts-check + +import tseslint from "typescript-eslint"; +import eslintPluginJest from "eslint-plugin-jest"; + +export default tseslint.config( + // Enable linting on TypeScript file extensions. + { + files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], + }, + + // Base ESLint rules + { + rules: { + "arrow-body-style": "error", + curly: ["error", "multi-line"], + eqeqeq: ["error", "always", { null: "ignore" }], + "no-caller": "error", + "no-cond-assign": "error", + "no-debugger": "error", + "no-duplicate-case": "error", + "no-new-wrappers": "error", + "no-restricted-globals": ["error", "parseInt", "parseFloat"], + "no-unused-labels": "error", + "no-var": "error", + "prefer-const": ["error", { destructuring: "all" }], + radix: "error", + "use-isnan": "error", + "object-shorthand": [ + "error", + "always", + { avoidQuotes: true, ignoreConstructors: false, avoidExplicitReturnArrows: true }, + ], + "no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "SequenceExpression"], + "spaced-comment": [ + "error", + "always", + { + line: { exceptions: ["-", "+"], markers: ["=", "!", "/"] }, + block: { exceptions: ["-", "+"], markers: ["=", "!", ":", "::"], balanced: true }, + }, + ], + "no-delete-var": ["error"], + "no-label-var": ["error"], + yoda: ["error"], + "prefer-numeric-literals": ["error"], + "prefer-rest-params": ["error"], + "prefer-spread": ["error"], + "no-useless-computed-key": ["error"], + "for-direction": ["error"], + "no-compare-neg-zero": ["error"], + "no-dupe-else-if": ["error"], + "no-empty": ["error", { allowEmptyCatch: true }], + "no-implicit-coercion": ["error", { boolean: true, number: true, string: true }], + "operator-assignment": ["error"], + "no-path-concat": ["error"], + "no-control-regex": ["error"], + "no-unneeded-ternary": ["error", { defaultAssignment: false }], + "one-var": ["error", "never"], + "prefer-exponentiation-operator": ["error"], + "prefer-object-spread": ["error"], + "no-useless-catch": ["error"], + "no-useless-concat": ["error"], + "no-useless-escape": ["error"], + "no-useless-return": ["error"], + }, + }, + + // typescript-eslint + { + plugins: { + "@typescript-eslint": tseslint.plugin, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: [ + "test/tsconfig.json", + "src/lualib/tsconfig.json", + "src/lualib/tsconfig.lua50.json", + "benchmark/tsconfig.json", + "language-extensions/tsconfig.json", + "tsconfig.eslint.json", + ], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": ["error", { default: "array-simple" }], + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/consistent-type-assertions": [ + "error", + { assertionStyle: "as", objectLiteralTypeAssertions: "never" }, + ], + "@typescript-eslint/consistent-type-definitions": "error", + "@typescript-eslint/explicit-member-accessibility": ["error", { overrides: { constructors: "no-public" } }], + "@typescript-eslint/naming-convention": [ + "error", + { + selector: "default", + format: ["camelCase"], + leadingUnderscore: "allow", + }, + { + selector: "variable", + format: ["camelCase", "UPPER_CASE"], + leadingUnderscore: "allow", + }, + { + selector: "typeLike", + format: ["PascalCase"], + }, + { + selector: "enumMember", + format: ["PascalCase"], + }, + { + selector: "typeParameter", + format: ["PascalCase"], + prefix: ["T"], + filter: { + regex: "K|V", + match: false, + }, + }, + { + selector: "interface", + format: ["PascalCase"], + custom: { + regex: "^I[A-Z]", + match: false, + }, + }, + { + // Ignore properties that require quotes + selector: "objectLiteralProperty", + modifiers: ["requiresQuotes"], + format: null, + }, + ], + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-empty-object-type": "error", + "@typescript-eslint/no-extra-non-null-assertion": "error", + "@typescript-eslint/no-extraneous-class": "error", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-inferrable-types": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/no-namespace": ["error", { allowDeclarations: true }], + "@typescript-eslint/no-this-alias": "error", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-arguments": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unsafe-function-type": "error", + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": "error", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-wrapper-object-types": "error", + "no-throw-literal": "off", + "@typescript-eslint/only-throw-error": "error", // "no-throw-literal" was renamed to "only-throw-error". + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/prefer-function-type": "error", + "@typescript-eslint/prefer-includes": "error", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/prefer-namespace-keyword": "error", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-string-starts-ends-with": "error", + "@typescript-eslint/promise-function-async": ["error", { checkArrowFunctions: false }], + "@typescript-eslint/require-await": "error", + "@typescript-eslint/restrict-plus-operands": ["error", { skipCompoundAssignments: false }], + "@typescript-eslint/return-await": "error", + "@typescript-eslint/triple-slash-reference": "error", + "@typescript-eslint/unified-signatures": "error", + }, + }, + + // eslint-plugin-jest + eslintPluginJest.configs["flat/recommended"], + eslintPluginJest.configs["flat/style"], + { + rules: { + "jest/expect-expect": "off", + "jest/consistent-test-it": ["error", { fn: "test", withinDescribe: "test" }], + "jest/no-disabled-tests": "error", + "jest/no-identical-title": "off", + "jest/no-test-return-statement": "error", + "jest/prefer-spy-on": "error", + "jest/prefer-todo": "error", + "jest/valid-title": "error", + // TODO: + // "jest/lowercase-name": "error", + }, + }, + + // Exceptions for specific files/directories + { + files: ["src/lualib/**/*.ts"], + rules: { + "no-restricted-syntax": ["error", "LabeledStatement", "SequenceExpression"], + "@typescript-eslint/only-throw-error": "off", + "@typescript-eslint/prefer-optional-chain": "off", + "@typescript-eslint/naming-convention": "off", + }, + }, + + // Ignore some specific files and directories + { + ignores: [ + ".github/scripts/create_benchmark_check.js", + "dist/", + "eslint.config.mjs", + "jest.config.js", + "test/cli/errors/", + "test/cli/watch/", + "test/translation/transformation/", + "test/transpile/directories/", + "test/transpile/module-resolution/**/node_modules/", + "test/transpile/module-resolution/**/dist/", + "test/transpile/outFile/", + "test/transpile/resolve-plugin/", + ], + } +); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..26b60c1cf --- /dev/null +++ b/jest.config.js @@ -0,0 +1,27 @@ +const isCI = process.env.GITHUB_ACTIONS !== undefined; + +/** @type {Partial} */ +module.exports = { + testMatch: ["**/test/**/*.spec.ts"], + collectCoverageFrom: [ + "/src/**/*", + "!/src/lualib/**/*", + // https://github.com/facebook/jest/issues/5274 + "!/src/tstl.ts", + ], + watchPathIgnorePatterns: ["cli/watch/[^/]+$", "src/lualib"], + + setupFilesAfterEnv: ["/test/setup.ts"], + testEnvironment: "node", + testRunner: "jest-circus/runner", + preset: "ts-jest", + transform: { + "^.+\\.ts?$": [ + "ts-jest", + { + tsconfig: "/test/tsconfig.json", + diagnostics: { warnOnly: !isCI }, + }, + ], + }, +}; diff --git a/language-extensions/index.d.ts b/language-extensions/index.d.ts new file mode 100644 index 000000000..6d805f189 --- /dev/null +++ b/language-extensions/index.d.ts @@ -0,0 +1,697 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +type AnyTable = Record; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions,@typescript-eslint/no-empty-object-type +type AnyNotNil = {}; + +/** + * Indicates a type is a language extension provided by TypescriptToLua when used as a value or function call. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TBrand A string used to uniquely identify the language extension type + */ +declare interface LuaExtension { + readonly __tstlExtension: TBrand; +} + +/** + * Indicates a type is a language extension provided by TypescriptToLua when used in a for-of loop. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TBrand A string used to uniquely identify the language extension type + */ +declare interface LuaIterationExtension { + readonly __tstlIterable: TBrand; +} + +/** + * Returns multiple values from a function, by wrapping them in a LuaMultiReturn tuple. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param T A tuple type with each element type representing a return value's type. + * @param values Return values. + */ +declare const $multi: ((...values: T) => LuaMultiReturn) & LuaExtension<"MultiFunction">; + +/** + * Represents multiple return values as a tuple. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param T A tuple type with each element type representing a return value's type. + */ +declare type LuaMultiReturn = T & { + readonly __tstlMultiReturn: any; +}; + +/** + * Creates a Lua-style numeric for loop (for i=start,limit,step) when used in for...of. Not valid in any other context. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param start The first number in the sequence to iterate over. + * @param limit The last number in the sequence to iterate over. + * @param step The amount to increment each iteration. + */ +declare const $range: ((start: number, limit: number, step?: number) => Iterable) & + LuaExtension<"RangeFunction">; + +/** + * Transpiles to the global vararg (`...`) + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + */ +declare const $vararg: string[] & LuaExtension<"VarargConstant">; + +/** + * Represents a Lua-style iterator which is returned from a LuaIterable. + * For simple iterators (with no state), this is just a function. + * For complex iterators that use a state, this is a LuaMultiReturn tuple containing a function, the state, and the initial value to pass to the function. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param state The state object returned from the LuaIterable. + * @param lastValue The last value returned from this function. If iterating LuaMultiReturn values, this is the first value of the tuple. + */ +declare type LuaIterator = TState extends undefined + ? (this: void) => TValue + : LuaMultiReturn< + [ + ( + this: void, + state: TState, + lastValue: TValue extends LuaMultiReturn ? TTuple[0] : TValue + ) => TValue, + TState, + TValue extends LuaMultiReturn ? TTuple[0] : TValue + ] + >; + +/** + * Represents a Lua-style iteratable which iterates single values in a `for...in` loop (ex. `for x in iter() do`). + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TValue The type of value returned each iteration. If this is LuaMultiReturn, multiple values will be returned each iteration. + * @param TState The type of the state value passed back to the iterator function each iteration. + */ +declare type LuaIterable = Iterable & + LuaIterator & + LuaIterationExtension<"Iterable">; + +/** + * Represents an object that can be iterated with pairs() + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the key returned each iteration. + * @param TValue The type of the value returned each iteration. + */ +declare type LuaPairsIterable = Iterable<[TKey, TValue]> & + LuaIterationExtension<"Pairs">; + +/** + * Represents an object that can be iterated with pairs(), where only the key value is used. + * + * @param TKey The type of the key returned each iteration. + */ +declare type LuaPairsKeyIterable = Iterable & LuaIterationExtension<"PairsKey">; + +/** + * Calls to functions with this type are translated to `left + right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaAddition = ((left: TLeft, right: TRight) => TReturn) & LuaExtension<"Addition">; + +/** + * Calls to methods with this type are translated to `left + right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaAdditionMethod = ((right: TRight) => TReturn) & LuaExtension<"AdditionMethod">; + +/** + * Calls to functions with this type are translated to `left - right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaSubtraction = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"Subtraction">; + +/** + * Calls to methods with this type are translated to `left - right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaSubtractionMethod = ((right: TRight) => TReturn) & LuaExtension<"SubtractionMethod">; + +/** + * Calls to functions with this type are translated to `left * right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaMultiplication = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"Multiplication">; + +/** + * Calls to methods with this type are translated to `left * right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaMultiplicationMethod = ((right: TRight) => TReturn) & + LuaExtension<"MultiplicationMethod">; + +/** + * Calls to functions with this type are translated to `left / right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaDivision = ((left: TLeft, right: TRight) => TReturn) & LuaExtension<"Division">; + +/** + * Calls to methods with this type are translated to `left / right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaDivisionMethod = ((right: TRight) => TReturn) & LuaExtension<"DivisionMethod">; + +/** + * Calls to functions with this type are translated to `left % right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaModulo = ((left: TLeft, right: TRight) => TReturn) & LuaExtension<"Modulo">; + +/** + * Calls to methods with this type are translated to `left % right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaModuloMethod = ((right: TRight) => TReturn) & LuaExtension<"ModuloMethod">; + +/** + * Calls to functions with this type are translated to `left ^ right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaPower = ((left: TLeft, right: TRight) => TReturn) & LuaExtension<"Power">; + +/** + * Calls to methods with this type are translated to `left ^ right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaPowerMethod = ((right: TRight) => TReturn) & LuaExtension<"PowerMethod">; + +/** + * Calls to functions with this type are translated to `left // right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaFloorDivision = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"FloorDivision">; + +/** + * Calls to methods with this type are translated to `left // right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaFloorDivisionMethod = ((right: TRight) => TReturn) & + LuaExtension<"FloorDivisionMethod">; + +/** + * Calls to functions with this type are translated to `left & right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseAnd = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"BitwiseAnd">; + +/** + * Calls to methods with this type are translated to `left & right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseAndMethod = ((right: TRight) => TReturn) & LuaExtension<"BitwiseAndMethod">; + +/** + * Calls to functions with this type are translated to `left | right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseOr = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"BitwiseOr">; + +/** + * Calls to methods with this type are translated to `left | right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseOrMethod = ((right: TRight) => TReturn) & LuaExtension<"BitwiseOrMethod">; + +/** + * Calls to functions with this type are translated to `left ~ right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseExclusiveOr = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"BitwiseExclusiveOr">; + +/** + * Calls to methods with this type are translated to `left ~ right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseExclusiveOrMethod = ((right: TRight) => TReturn) & + LuaExtension<"BitwiseExclusiveOrMethod">; + +/** + * Calls to functions with this type are translated to `left << right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseLeftShift = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"BitwiseLeftShift">; + +/** + * Calls to methods with this type are translated to `left << right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseLeftShiftMethod = ((right: TRight) => TReturn) & + LuaExtension<"BitwiseLeftShiftMethod">; + +/** + * Calls to functions with this type are translated to `left >> right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseRightShift = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"BitwiseRightShift">; + +/** + * Calls to methods with this type are translated to `left >> right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseRightShiftMethod = ((right: TRight) => TReturn) & + LuaExtension<"BitwiseRightShiftMethod">; + +/** + * Calls to functions with this type are translated to `left .. right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaConcat = ((left: TLeft, right: TRight) => TReturn) & LuaExtension<"Concat">; + +/** + * Calls to methods with this type are translated to `left .. right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaConcatMethod = ((right: TRight) => TReturn) & LuaExtension<"ConcatMethod">; + +/** + * Calls to functions with this type are translated to `left < right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaLessThan = ((left: TLeft, right: TRight) => TReturn) & LuaExtension<"LessThan">; + +/** + * Calls to methods with this type are translated to `left < right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaLessThanMethod = ((right: TRight) => TReturn) & LuaExtension<"LessThanMethod">; + +/** + * Calls to functions with this type are translated to `left > right`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TLeft The type of the left-hand-side of the operation. + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaGreaterThan = ((left: TLeft, right: TRight) => TReturn) & + LuaExtension<"GreaterThan">; + +/** + * Calls to methods with this type are translated to `left > right`, where `left` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TRight The type of the right-hand-side of the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaGreaterThanMethod = ((right: TRight) => TReturn) & LuaExtension<"GreaterThanMethod">; + +/** + * Calls to functions with this type are translated to `-operand`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TOperand The type of the value in the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaNegation = ((operand: TOperand) => TReturn) & LuaExtension<"Negation">; + +/** + * Calls to method with this type are translated to `-operand`, where `operand` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaNegationMethod = (() => TReturn) & LuaExtension<"NegationMethod">; + +/** + * Calls to functions with this type are translated to `~operand`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TOperand The type of the value in the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseNot = ((operand: TOperand) => TReturn) & LuaExtension<"BitwiseNot">; + +/** + * Calls to method with this type are translated to `~operand`, where `operand` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaBitwiseNotMethod = (() => TReturn) & LuaExtension<"BitwiseNotMethod">; + +/** + * Calls to functions with this type are translated to `#operand`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TOperand The type of the value in the operation. + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaLength = ((operand: TOperand) => TReturn) & LuaExtension<"Length">; + +/** + * Calls to method with this type are translated to `#operand`, where `operand` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TReturn The resulting (return) type of the operation. + */ +declare type LuaLengthMethod = (() => TReturn) & LuaExtension<"LengthMethod">; + +/** + * Calls to functions with this type are translated to `table[key]`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TTable The type to access as a Lua table. + * @param TKey The type of the key to use to access the table. + * @param TValue The type of the value stored in the table. + */ +declare type LuaTableGet = (( + table: TTable, + key: TKey +) => TValue) & + LuaExtension<"TableGet">; + +/** + * Calls to methods with this type are translated to `table[key]`, where `table` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the key to use to access the table. + * @param TValue The type of the value stored in the table. + */ +declare type LuaTableGetMethod = ((key: TKey) => TValue) & + LuaExtension<"TableGetMethod">; + +/** + * Calls to functions with this type are translated to `table[key] = value`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TTable The type to access as a Lua table. + * @param TKey The type of the key to use to access the table. + * @param TValue The type of the value to assign to the table. + */ +declare type LuaTableSet = (( + table: TTable, + key: TKey, + value: TValue +) => void) & + LuaExtension<"TableSet">; + +/** + * Calls to methods with this type are translated to `table[key] = value`, where `table` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the key to use to access the table. + * @param TValue The type of the value to assign to the table. + */ +declare type LuaTableSetMethod = ((key: TKey, value: TValue) => void) & + LuaExtension<"TableSetMethod">; + +/** + * Calls to functions with this type are translated to `table[key] = true`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TTable The type to access as a Lua table. + * @param TKey The type of the key to use to access the table. + */ +declare type LuaTableAddKey = ((table: TTable, key: TKey) => void) & + LuaExtension<"TableAddKey">; + +/** + * Calls to methods with this type are translated to `table[key] = true`, where `table` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * @param TKey The type of the key to use to access the table. + */ +declare type LuaTableAddKeyMethod = ((key: TKey) => void) & LuaExtension<"TableAddKeyMethod">; + +/** + * Calls to functions with this type are translated to `table[key] ~= nil`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TTable The type to access as a Lua table. + * @param TKey The type of the key to use to access the table. + */ +declare type LuaTableHas = ((table: TTable, key: TKey) => boolean) & + LuaExtension<"TableHas">; + +/** + * Calls to methods with this type are translated to `table[key] ~= nil`, where `table` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the key to use to access the table. + */ +declare type LuaTableHasMethod = ((key: TKey) => boolean) & LuaExtension<"TableHasMethod">; + +/** + * Calls to functions with this type are translated to `table[key] = nil`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TTable The type to access as a Lua table. + * @param TKey The type of the key to use to access the table. + */ +declare type LuaTableDelete = ((table: TTable, key: TKey) => boolean) & + LuaExtension<"TableDelete">; + +/** + * Calls to methods with this type are translated to `table[key] = nil`, where `table` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the key to use to access the table. + */ +declare type LuaTableDeleteMethod = ((key: TKey) => boolean) & + LuaExtension<"TableDeleteMethod">; + +/** + * Calls to functions with this type are translated to `next(myTable) == nil`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TTable The type to access as a Lua table. + */ +declare type LuaTableIsEmpty = ((table: TTable) => boolean) & LuaExtension<"TableIsEmpty">; + +/** + * Calls to methods with this type are translated to `next(myTable) == nil`, where `table` is the object with the method. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + */ +declare type LuaTableIsEmptyMethod = (() => boolean) & LuaExtension<"TableIsEmptyMethod">; + +/** + * A convenience type for working directly with a Lua table. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the keys used to access the table. + * @param TValue The type of the values stored in the table. + */ +declare interface LuaTable extends LuaPairsIterable { + length: LuaLengthMethod; + get: LuaTableGetMethod; + set: LuaTableSetMethod; + has: LuaTableHasMethod; + delete: LuaTableDeleteMethod; + isEmpty: LuaTableIsEmptyMethod; +} + +/** + * A convenience type for working directly with a Lua table. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the keys used to access the table. + * @param TValue The type of the values stored in the table. + */ +declare type LuaTableConstructor = (new () => LuaTable< + TKey, + TValue +>) & + LuaExtension<"TableNew">; + +/** + * A convenience type for working directly with a Lua table. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * + * @param TKey The type of the keys used to access the table. + * @param TValue The type of the values stored in the table. + */ +declare const LuaTable: LuaTableConstructor; + +/** + * A convenience type for working directly with a Lua table, used as a map. + * + * This differs from LuaTable in that the `get` method may return `nil`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * @param K The type of the keys used to access the table. + * @param V The type of the values stored in the table. + */ +declare interface LuaMap extends LuaPairsIterable { + get: LuaTableGetMethod; + set: LuaTableSetMethod; + has: LuaTableHasMethod; + delete: LuaTableDeleteMethod; + isEmpty: LuaTableIsEmptyMethod; +} + +/** + * A convenience type for working directly with a Lua table, used as a map. + * + * This differs from LuaTable in that the `get` method may return `nil`. + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * @param K The type of the keys used to access the table. + * @param V The type of the values stored in the table. + */ +declare const LuaMap: (new () => LuaMap) & LuaExtension<"TableNew">; + +/** + * Readonly version of {@link LuaMap}. + * + * @param K The type of the keys used to access the table. + * @param V The type of the values stored in the table. + */ +declare interface ReadonlyLuaMap extends LuaPairsIterable { + get: LuaTableGetMethod; + has: LuaTableHasMethod; + isEmpty: LuaTableIsEmptyMethod; +} + +/** + * A convenience type for working directly with a Lua table, used as a set. + * + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * @param T The type of the keys used to access the table. + */ +declare interface LuaSet extends LuaPairsKeyIterable { + add: LuaTableAddKeyMethod; + has: LuaTableHasMethod; + delete: LuaTableDeleteMethod; + isEmpty: LuaTableIsEmptyMethod; +} + +/** + * A convenience type for working directly with a Lua table, used as a set. + * + * For more information see: https://typescripttolua.github.io/docs/advanced/language-extensions + * @param T The type of the keys used to access the table. + */ +declare const LuaSet: (new () => LuaSet) & LuaExtension<"TableNew">; + +/** + * Readonly version of {@link LuaSet}. + * + * @param T The type of the keys used to access the table. + */ +declare interface ReadonlyLuaSet extends LuaPairsKeyIterable { + has: LuaTableHasMethod; + isEmpty: LuaTableIsEmptyMethod; +} + +interface ObjectConstructor { + /** Returns an array of keys of an object, when iterated with `pairs`. */ + keys(o: LuaPairsIterable | LuaPairsKeyIterable): K[]; + + /** Returns an array of values of an object, when iterated with `pairs`. */ + values(o: LuaPairsIterable): V[]; + + /** Returns an array of key/values of an object, when iterated with `pairs`. */ + entries(o: LuaPairsIterable): Array<[K, V]>; +} diff --git a/language-extensions/package.json b/language-extensions/package.json new file mode 100644 index 000000000..194ab1212 --- /dev/null +++ b/language-extensions/package.json @@ -0,0 +1,23 @@ +{ + "name": "@typescript-to-lua/language-extensions", + "version": "1.19.0", + "description": "Language extensions used by typescript-to-lua", + "repository": "https://github.com/TypeScriptToLua/TypeScriptToLua", + "homepage": "https://typescripttolua.github.io/", + "bugs": { + "url": "https://github.com/TypeScriptToLua/TypeScriptToLua/issues" + }, + "license": "MIT", + "keywords": [ + "typescript", + "lua", + "tstl", + "transpiler", + "language extensions" + ], + "files": [ + "**.d.ts" + ], + "main": "", + "types": "index.d.ts" +} diff --git a/language-extensions/tsconfig.json b/language-extensions/tsconfig.json new file mode 100644 index 000000000..92894f9c4 --- /dev/null +++ b/language-extensions/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "strict": true + } +} diff --git a/logo-hq.png b/logo-hq.png new file mode 100644 index 000000000..a60542a97 Binary files /dev/null and b/logo-hq.png differ diff --git a/package-lock.json b/package-lock.json index 420d71821..cbe9690d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2323 +1,5960 @@ { "name": "typescript-to-lua", - "version": "0.15.2", - "lockfileVersion": 1, + "version": "1.33.2", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" + "packages": { + "": { + "name": "typescript-to-lua", + "version": "1.33.2", + "license": "MIT", + "dependencies": { + "@typescript-to-lua/language-extensions": "1.19.0", + "enhanced-resolve": "5.8.2", + "picomatch": "^2.3.1", + "resolve": "^1.15.1", + "source-map": "^0.7.3" + }, + "bin": { + "tstl": "dist/tstl.js" + }, + "devDependencies": { + "@types/fs-extra": "^8.1.0", + "@types/glob": "^7.2.0", + "@types/jest": "^27.5.2", + "@types/node": "^22.10.0", + "@types/picomatch": "^2.3.0", + "@types/resolve": "1.14.0", + "eslint": "^9.22.0", + "eslint-plugin-jest": "^28.8.3", + "fs-extra": "^8.1.0", + "javascript-stringify": "^2.0.1", + "jest": "^29.5.0", + "jest-circus": "^29.7.0", + "lua-types": "^2.13.0", + "lua-wasm-bindings": "^0.5.3", + "prettier": "^2.8.8", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "5.9.3", + "typescript-eslint": "^8.46.3" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "typescript": "5.9.3" } }, - "@babel/generator": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz", - "integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, - "requires": { - "@babel/types": "^7.3.2", - "jsesc": "^2.5.1", - "lodash": "^4.17.10", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "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": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "node_modules/@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", - "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "node_modules/@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - }, + "license": "MIT", "dependencies": { - "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, - "requires": { - "color-convert": "^1.9.0" - } - }, - "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, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "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, - "requires": { - "has-flag": "^3.0.0" - } - } + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/parser": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz", - "integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==", - "dev": true - }, - "@babel/template": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", - "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.2.2", - "@babel/types": "^7.2.2" + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/traverse": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", - "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.2.2", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.2.3", - "@babel/types": "^7.2.2", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.10" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "@babel/types": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz", - "integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==", + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.10", - "to-fast-properties": "^2.0.0" + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "@types/events": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true - }, - "@types/glob": { - "version": "5.0.35", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz", - "integrity": "sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/node": { - "version": "9.6.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.23.tgz", - "integrity": "sha512-d2SJJpwkiPudEQ3+9ysANN2Nvz4QJKUPoe/WL5zyQzI0RaEeZWH5K5xjvUIGszTItHQpFPdH+u51f6G/LkS8Cg==", - "dev": true - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "alsatian": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/alsatian/-/alsatian-2.3.0.tgz", - "integrity": "sha512-e83K7JpH9Hj1+TUYyZialNKDln5gW56C82CVcd0AMt2whLFtKzYiddE5Dz2biUWjT8KzF11lkwonzf9wOCQyAA==", + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, - "requires": { - "@types/node": ">=4.0.0", - "extendo-error": "^1.0.1", - "glob": "^7.0.3", - "reflect-metadata": "^0.1.3", - "tap-bark": "1.0.0" + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@babel/helper-string-parser": { + "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, - "requires": { - "sprintf-js": "~1.0.2" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "node_modules/@babel/helper-validator-identifier": { + "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, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" } }, - "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "codecov": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.0.2.tgz", - "integrity": "sha512-9ljtIROIjPIUmMRqO+XuDITDoV8xRrZmA0jcEq6p2hg2+wY9wGmLfreAZGIL72IzUfdEDZaU8+Vjidg1fBQ8GQ==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "requires": { - "argv": "0.0.2", - "request": "^2.81.0", - "urlgrey": "0.4.4" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "color-convert": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", - "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, - "requires": { - "color-name": "1.1.1" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "requires": { - "delayed-stream": "~1.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "commander": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", - "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "requires": { - "assert-plus": "^1.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", "dev": true, - "requires": { - "ms": "^2.1.1" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "diff": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", - "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", - "dev": true - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "eventemitter3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", - "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=", - "dev": true - }, - "events-to-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", - "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extendo-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/extendo-error/-/extendo-error-1.0.1.tgz", - "integrity": "sha1-MDJeDPbC+l+RAj0hAO0CGCbmMX0=", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fengari": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.2.tgz", - "integrity": "sha512-NleQtQymPtbjBPnGOQiXfZfKpP3R7si+Sbkke631PWNnZt86eZSlymRJ0qv/KUTcz0fSsWO1iltZYz5/JV1uYQ==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "requires": { - "readline-sync": "^1.4.9", - "sprintf-js": "^1.1.1", - "tmp": "^0.0.33" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "license": "MIT", "dependencies": { - "sprintf-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", - "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=", - "dev": true - } + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "flatted": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", - "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", - "dev": true + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "requires": { - "assert-plus": "^1.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "globals": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", - "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", - "dev": true + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dev": true, - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, - "requires": { - "ansi-regex": "^2.0.0" + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "node_modules/@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + } }, - "istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==", - "dev": true + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "istanbul-lib-instrument": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz", - "integrity": "sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA==", - "dev": true, - "requires": { - "@babel/generator": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "istanbul-lib-coverage": "^2.0.3", - "semver": "^5.5.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "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, - "optional": true + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "node_modules/@eslint/core": { + "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" + } }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "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.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": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "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" + } }, - "json-schema-traverse": { + "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/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true + "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" + } }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } }, - "make-error": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", - "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", - "dev": true + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "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, - "requires": { - "mime-db": "~1.33.0" + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "requires": { - "brace-expansion": "^1.1.7" + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, - "requires": { - "minimist": "0.0.8" + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true } } }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", - "dev": true + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "nyc": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.3.0.tgz", - "integrity": "sha512-P+FwIuro2aFG6B0Esd9ZDWUd51uZrAEoGutqZxzrVmYl3qSfkLgcQpBPBjtDFsUQLFY1dvTQJPOyeqr8S9GF8w==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "arrify": "^1.0.1", - "caching-transform": "^3.0.1", - "convert-source-map": "^1.6.0", - "find-cache-dir": "^2.0.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.3", - "istanbul-lib-hook": "^2.0.3", - "istanbul-lib-instrument": "^3.1.0", - "istanbul-lib-report": "^2.0.4", - "istanbul-lib-source-maps": "^3.0.2", - "istanbul-reports": "^2.1.1", - "make-dir": "^1.3.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.3", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.1.0", - "uuid": "^3.3.2", - "yargs": "^12.0.5", - "yargs-parser": "^11.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "append-transform": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, - "archy": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "arrify": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "async": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "caching-transform": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "hasha": "^3.0.0", - "make-dir": "^1.3.0", - "package-hash": "^3.0.0", - "write-file-atomic": "^2.3.0" - } - }, - "camelcase": { - "version": "5.0.0", - "bundled": true, - "dev": true - }, - "cliui": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "commander": { - "version": "2.17.1", - "bundled": true, - "dev": true, + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { "optional": true - }, - "commondir": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cross-spawn": { - "version": "4.0.2", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "default-require-extensions": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, - "end-of-stream": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "error-ex": { - "version": "1.3.2", - "bundled": true, - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es6-error": { - "version": "4.1.1", - "bundled": true, - "dev": true - }, - "execa": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "bundled": true, - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - } - } - }, - "find-cache-dir": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "foreground-child": { - "version": "1.5.6", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.15", - "bundled": true, - "dev": true - }, - "handlebars": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "async": "^2.5.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } - } - }, - "has-flag": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "hasha": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "is-stream": "^1.0.1" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "bundled": true, - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "bundled": true, - "dev": true - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "invert-kv": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "isexe": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "istanbul-lib-coverage": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "istanbul-lib-hook": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, - "istanbul-lib-report": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.3", - "make-dir": "^1.3.0", - "supports-color": "^6.0.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "bundled": true, - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.3", - "make-dir": "^1.3.0", - "rimraf": "^2.6.2", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } - } - }, - "istanbul-reports": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "handlebars": "^4.1.0" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "lcid": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.11", - "bundled": true, - "dev": true - }, - "lodash.flattendeep": { - "version": "4.4.0", - "bundled": true, - "dev": true - }, - "lru-cache": { - "version": "4.1.5", - "bundled": true, - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "make-dir": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "mem": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^2.0.0" - } - }, - "merge-source-map": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } - } - }, - "mimic-fn": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.10", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - } - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "bundled": true, - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "bundled": true, - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-defer": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "p-is-promise": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "p-limit": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "package-hash": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^3.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-exists": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "path-key": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "bundled": true, - "dev": true - }, - "path-type": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "pump": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "read-pkg": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "release-zalgo": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "require-directory": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "resolve": { - "version": "1.10.0", - "bundled": true, - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "bundled": true, - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.6.0", - "bundled": true, - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "spawn-wrap": { - "version": "1.4.2", - "bundled": true, - "dev": true, - "requires": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "which": "^1.3.0" - } - }, - "spdx-correct": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "bundled": true, - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.3", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "test-exclude": { - "version": "5.1.0", - "bundled": true, - "dev": true, - "requires": { - "arrify": "^1.0.1", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^1.0.1" - } - }, - "uglify-js": { - "version": "3.4.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "uuid": { - "version": "3.3.2", - "bundled": true, - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "bundled": true, - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "bundled": true, - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "write-file-atomic": { - "version": "2.4.2", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "y18n": { - "version": "4.0.0", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "2.1.2", - "bundled": true, - "dev": true - }, - "yargs": { - "version": "12.0.5", - "bundled": true, - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "bundled": true, - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@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", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.0", + "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" + } + }, + "node_modules/@types/picomatch": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", + "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.14.0.tgz", + "integrity": "sha512-bmjNBW6tok+67iOsASeYSJxSgY++BIR35nGyGLORTDirhra9reJ0shgGL3U7KPDUbOBCx8JrlCjd4d/y5uiMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-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", + "integrity": "sha512-Os5wOKwviTD4LeqI29N0btYOjokSJ97iCf45EOjIABlb5IwNQy7AE/AqZJobRw3ywHH8+KzJUMkEirWPzh2tUA==", + "license": "MIT" + }, + "node_modules/acorn": { + "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" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001663", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", + "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "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": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", + "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.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", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "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": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "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" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "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.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expect/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.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", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "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": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/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/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "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", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-diff/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "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": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/lua-wasm-bindings": { + "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": { + "@types/semver": "^7.3.9", + "semver": "^7.3.7" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "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", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "requires": { - "wrappy": "1" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "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" }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readline-sync": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.9.tgz", - "integrity": "sha1-PtqOZfI80qF+YTAbHwADOWr17No=", - "dev": true + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "reflect-metadata": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", - "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", - "dev": true + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } - }, - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" } }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "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 + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } }, - "source-map-support": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", - "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "sprintf-js": { + "node_modules/source-map-support/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/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "requires": { - "safe-buffer": "~5.1.0" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, - "requires": { - "ansi-regex": "^2.0.0" + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } }, - "tap-bark": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tap-bark/-/tap-bark-1.0.0.tgz", - "integrity": "sha1-bAPcUWh/7Xh3+COtSx3dHvXrVnQ=", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { - "@types/node": ">=0.0.2", - "chalk": "^1.1.3", - "duplexer": "^0.1.1", - "tap-parser": "^3.0.3", - "through2": "^2.0.1" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "tap-parser": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-3.0.5.tgz", - "integrity": "sha1-uUf2ngs+U9S5IBH2zFUuFtrcfsk=", + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, - "requires": { - "events-to-array": "^1.0.1", - "js-yaml": "^3.2.7", - "readable-stream": "^2" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "threads": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/threads/-/threads-0.12.0.tgz", - "integrity": "sha512-4B7hd61lDsVW1Z/+FAVX7D9QbiQYUbtGMHVkkwWT/nKPKas8u4FEc+Rg8E8h2erhNTQGNqNJ0TsholmhpKNPRg==", + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, - "requires": { - "eventemitter3": "^2.0.2", - "native-promise-only": "^0.8.1" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "requires": { - "punycode": "^1.4.1" + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true + "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" }, - "ts-node": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.0.tgz", - "integrity": "sha512-klJsfswHP0FuOLsvBZ/zzCfUvakOSSxds78mVeK7I+qP76YWtxf16hEZsp3U+b0kIo82R5UatGFeblYMqabb2Q==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { - "arrify": "^1.0.0", - "buffer-from": "^1.1.0", - "diff": "^3.1.0", - "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.5.6", - "yn": "^2.0.0" + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } }, - "tslint": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", - "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", - "dev": true, - "requires": { - "babel-code-frame": "^6.22.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^3.2.0", - "glob": "^7.1.1", - "js-yaml": "^3.7.0", - "minimatch": "^3.0.4", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.27.2" - }, - "dependencies": { - "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, - "requires": { - "color-convert": "^1.9.0" - } + "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==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "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" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } + "@jest/transform": { + "optional": true }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "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", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true } } }, - "tsutils": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", - "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "requires": { - "tslib": "^1.8.1" + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, - "requires": { - "safe-buffer": "^5.0.1" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "optional": true + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "typescript": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.1.tgz", - "integrity": "sha512-cTmIDFW7O0IHbn1DPYjkiebHxwtCMU+eTy30ZtJNBPF9j2O1ITu5XH2YnBeVRKWHqF+3JQwWJv0Q0aUgX8W7IA==" + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, - "urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true + "node_modules/typescript-eslint": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", + "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } }, - "yn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", - "dev": true + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 200f4f278..e6a53fca5 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "name": "typescript-to-lua", + "version": "1.33.2", + "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/", + "bugs": { + "url": "https://github.com/TypeScriptToLua/TypeScriptToLua/issues" + }, "license": "MIT", - "version": "0.15.2", - "repository": "https://github.com/Perryvw/TypescriptToLua", "keywords": [ "typescript", "lua", @@ -12,62 +17,59 @@ "files": [ "dist/**/*.js", "dist/**/*.lua", - "dist/**/*.ts" + "dist/**/*.ts", + "dist/lualib/**/*.json" ], "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "tsc -p tsconfig.json && npm run build-lualib", - "build-lualib": "ts-node ./build_lualib.ts", - "pretest": "npm run clean && npm run style-check && npm run build && tsc -p ./test/tsconfig.json", - "test": "node ./test/runner.js", - "posttest": "npm run clean", - "coverage": "nyc --source-map=true npm test && nyc report --reporter=text-lcov > coverage.lcov", - "coverage-html": "nyc --source-map=true npm test && nyc report --reporter=html", - "test-threaded": "npm run pretest && node ./test/threaded_runner.js && npm run posttest", - "test-fast": "npm run pretest && node ./test/runner.js --ignoreDiagnostics && npm run posttest", - "clean": "rimraf \"src/**/*.js\" \"src/**/*.js.map\" \"test/**/*.js\" \"test/**/*.js.map\" \"test/compiler/testfiles/*.lua\"", - "release-patch": "npm version patch", - "release-minor": "npm version minor", - "release-major": "npm version major", + "build": "tsc && npm run build-lualib", + "build-lualib": "node dist/tstl.js -p src/lualib/tsconfig.json && node dist/tstl.js -p src/lualib/tsconfig.lua50.json", + "pretest": "npm run lint && npm run check:language-extensions && npm run build-lualib", + "test": "jest", + "lint": "npm run lint:eslint && npm run lint:prettier", + "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", "preversion": "npm run build && npm test", - "postversion": "git push && git push --tags", - "style-check": "tslint -p . && tslint -c ./tslint.json src/lualib/*.ts" + "postversion": "git push && git push --tags" }, "bin": { - "tstl": "./dist/tstl.js" - }, - "nyc": { - "all": true, - "extension": [ - ".ts" - ], - "include": [ - "src/**/*" - ], - "exclude": [ - "src/lualib/*" - ] + "tstl": "dist/tstl.js" }, "engines": { - "node": ">=8.5.0" + "node": ">=16.10.0" + }, + "peerDependencies": { + "typescript": "5.9.3" }, "dependencies": { - "typescript": "^3.3.1" + "@typescript-to-lua/language-extensions": "1.19.0", + "enhanced-resolve": "5.8.2", + "picomatch": "^2.3.1", + "resolve": "^1.15.1", + "source-map": "^0.7.3" }, "devDependencies": { - "@types/glob": "^5.0.35", - "@types/node": "^9.6.23", - "alsatian": "^2.3.0", - "codecov": "3.0.2", - "deep-equal": "^1.0.1", - "fengari": "^0.1.2", - "flatted": "^2.0.0", - "glob": "^7.1.2", - "nyc": "^13.3.0", - "rimraf": "^2.6.3", - "threads": "^0.12.0", - "ts-node": "^7.0.0", - "tslint": "^5.10.0" + "@types/fs-extra": "^8.1.0", + "@types/glob": "^7.2.0", + "@types/jest": "^27.5.2", + "@types/node": "^22.10.0", + "@types/picomatch": "^2.3.0", + "@types/resolve": "1.14.0", + "eslint": "^9.22.0", + "eslint-plugin-jest": "^28.8.3", + "fs-extra": "^8.1.0", + "javascript-stringify": "^2.0.1", + "jest": "^29.5.0", + "jest-circus": "^29.7.0", + "lua-types": "^2.13.0", + "lua-wasm-bindings": "^0.5.3", + "prettier": "^2.8.8", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "5.9.3", + "typescript-eslint": "^8.46.3" } } diff --git a/src/CommandLineParser.ts b/src/CommandLineParser.ts deleted file mode 100644 index 4d631a501..000000000 --- a/src/CommandLineParser.ts +++ /dev/null @@ -1,354 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; - -import {CompilerOptions, LuaTarget, LuaLibImportKind} from "./CompilerOptions"; - -export type CLIParseResult = ParseResult; - -type ParseResult = - { isValid: true; result: T } - | { isValid: false, errorMessage: string}; - -interface ParsedCommandLine extends ts.ParsedCommandLine { - options: CompilerOptions; -} - -interface BaseCLIOption { - aliases: string[]; - describe: string; - type: string; -} - -interface CLIOption extends BaseCLIOption { - choices: T[]; - default: T; -} - -const optionDeclarations: {[key: string]: CLIOption} = { - luaLibImport: { - choices: [LuaLibImportKind.Inline, LuaLibImportKind.Require, LuaLibImportKind.Always, LuaLibImportKind.None], - default: LuaLibImportKind.Inline, - describe: "Specifies how js standard features missing in lua are imported.", - type: "enum", - } as CLIOption, - luaTarget: { - aliases: ["lt"], - choices: [LuaTarget.LuaJIT, LuaTarget.Lua53, LuaTarget.Lua52, LuaTarget.Lua51], - default: LuaTarget.LuaJIT, - describe: "Specify Lua target version.", - type: "enum", - } as CLIOption, - noHeader: { - default: false, - describe: "Specify if a header will be added to compiled files.", - type: "boolean", - } as CLIOption, - noHoisting: { - default: false, - describe: "Disables hoisting.", - type: "boolean", - } as CLIOption, -}; - -export const { version } = require("../package.json"); - -const helpString = - `Version ${version}\n` + - "Syntax: tstl [options] [files...]\n\n" + - - "Examples: tstl path/to/file.ts [...]\n" + - " tstl -p path/to/tsconfig.json\n\n" + - - "In addition to the options listed below you can also pass options\n" + - "for the typescript compiler (For a list of options use tsc -h).\n" + - "Some tsc options might have no effect."; - -/** - * Parse the supplied arguments. - * The result will include arguments supplied via CLI and arguments from tsconfig. - */ -export function parseCommandLine(args: string[]): CLIParseResult -{ - let commandLine = ts.parseCommandLine(args); - - // Run diagnostics to check for invalid tsc options - const diagnosticsResult = runTsDiagnostics(commandLine); - if (diagnosticsResult.isValid === false) { - return diagnosticsResult; - } - - // This will add TS and TSTL options from a tsconfig - const configResult = readTsConfig(commandLine); - if (configResult.isValid === true) { - commandLine = configResult.result; - } else { - return { isValid: false, errorMessage: configResult.errorMessage }; - } - - // Run diagnostics to check for invalid tsconfig - const diagnosticsResult2 = runTsDiagnostics(commandLine); - if (diagnosticsResult2.isValid === false) { - return diagnosticsResult2; - } - - // Merge TSTL CLI options in (highest priority) will also set defaults if none specified - const tstlCLIResult = parseTSTLOptions(commandLine, args); - if (tstlCLIResult.isValid === true) { - commandLine = tstlCLIResult.result; - } else { - return { isValid: false, errorMessage: tstlCLIResult.errorMessage }; - } - - if (commandLine.options.project && !commandLine.options.rootDir) { - commandLine.options.rootDir = path.dirname(commandLine.options.project); - } - - if (!commandLine.options.rootDir) { - commandLine.options.rootDir = process.cwd(); - } - - if (!commandLine.options.outDir) { - commandLine.options.outDir = commandLine.options.rootDir; - } - - return { isValid: true, result: commandLine as ParsedCommandLine }; -} - -export function getHelpString(): string { - let result = helpString + "\n\n"; - - result += "Options:\n"; - for (const optionName in optionDeclarations) { - const option = optionDeclarations[optionName]; - const aliasStrings = option.aliases - ? option.aliases.map(a => "-" + a) - : []; - - const optionString = aliasStrings.concat(["--" + optionName]).join("|"); - - const parameterDescribe = option.choices - ? option.choices.join("|") - : option.type; - - const spacing = " ".repeat(Math.max(1, 45 - optionString.length - parameterDescribe.length)); - - result += `\n ${optionString} <${parameterDescribe}>${spacing}${option.describe}\n`; - } - - return result; -} - -function readTsConfig(parsedCommandLine: ts.ParsedCommandLine): CLIParseResult -{ - const options = parsedCommandLine.options; - - // Load config - if (options.project) { - const findProjectPathResult = findConfigFile(options); - if (findProjectPathResult.isValid === true) { - options.project = findProjectPathResult.result; - } else { - return { isValid: false, errorMessage: findProjectPathResult.errorMessage }; - } - - const configPath = options.project; - const configContents = fs.readFileSync(configPath).toString(); - const configJson = ts.parseConfigFileTextToJson(configPath, configContents); - const parsedJsonConfig = ts.parseJsonConfigFileContent( - configJson.config, - ts.sys, - path.dirname(configPath), - options - ); - - for (const key in parsedJsonConfig.raw) { - const option = optionDeclarations[key]; - if (option !== undefined) { - const value = readValue(parsedJsonConfig.raw[key], option.type, key); - if (option.choices) { - if (option.choices.indexOf(value) < 0) { - return { - isValid: false, - errorMessage: `Unknown ${key} value '${value}'.\nAccepted values: ${option.choices}`, - }; - } - } - - parsedJsonConfig.options[key] = value; - } - } - return { isValid: true, result: parsedJsonConfig }; - } - return { isValid: true, result: parsedCommandLine }; -} - -function parseTSTLOptions(commandLine: ts.ParsedCommandLine, args: string[]): CLIParseResult { - const result = {}; - for (let i = 0; i < args.length; i++) { - if (args[i].startsWith("--")) { - const argumentName = args[i].substr(2); - const option = optionDeclarations[argumentName]; - if (option) { - const argumentResult = getArgumentValue(argumentName, args[i + 1]); - i++; // Skip the value from being considered as argument name - if (argumentResult.isValid === true) { - result[argumentName] = argumentResult.result; - } else { - return { isValid: false, errorMessage: argumentResult.errorMessage }; - } - } - } else if (args[i].startsWith("-")) { - const argument = args[i].substr(1); - let argumentName: string; - for (const key in optionDeclarations) { - if (optionDeclarations[key].aliases && optionDeclarations[key].aliases.indexOf(argument) >= 0) { - argumentName = key; - break; - } - } - - if (argumentName) { - const argumentResult = getArgumentValue(argumentName, args[i + 1]); - i++; // Skip the value from being considered as argument name - if (argumentResult.isValid === true) { - result[argumentName] = argumentResult.result; - } else { - return { isValid: false, errorMessage: argumentResult.errorMessage }; - } - } - } - } - for (const option in result) { - commandLine.options[option] = result[option]; - } - // Add defaults if not set - const defaultOptions = getDefaultOptions(); - for (const option in defaultOptions) { - if (!commandLine.options[option]) { - commandLine.options[option] = defaultOptions[option]; - } - } - return { isValid: true, result: commandLine }; -} - -function getArgumentValue(argumentName: string, argument: string): ParseResult -{ - if (argument === undefined) { - return { isValid: false, errorMessage: `Missing value for parameter ${argumentName}`}; - } - - const option = optionDeclarations[argumentName]; - const value = readValue(argument, option.type, argumentName); - - if (option.choices) { - if (option.choices.indexOf(value) < 0) { - return { - isValid: false, - errorMessage: `Unknown ${argumentName} value '${value}'. Accepted values are: ${option.choices}`, - }; - } - } - - return { isValid: true, result: value }; -} - -function readValue(valueString: string, valueType: string, parameterName: string): string | boolean { - if (valueType === "boolean") { - return valueString === "true" || valueString === "t" - ? true - : false; - } else if (valueType === "enum") { - return valueString.toLowerCase(); - } else { - return valueString; - } -} - -function getDefaultOptions(): CompilerOptions { - const options: CompilerOptions = {}; - - for (const optionName in optionDeclarations) { - if (optionDeclarations[optionName].default !== undefined) { - options[optionName] = optionDeclarations[optionName].default; - } - } - - return options; -} - -/** Check the current state of the ParsedCommandLine for errors */ -function runTsDiagnostics(commandLine: ts.ParsedCommandLine): ParseResult { - // Remove files that dont exist - commandLine.fileNames = commandLine.fileNames.filter(file => fs.existsSync(file) || fs.existsSync(file + ".ts")); - - const tsInvalidCompilerOptionErrorCode = 5023; - if (commandLine.errors.length !== 0) { - // Generate a list of valid option names and aliases - const optionNames: string[] = []; - for (const key of Object.keys(optionDeclarations)) { - optionNames.push(key); - const alias = optionDeclarations[key].aliases; - if (alias) { - if (typeof alias === "string") { - optionNames.push(alias); - } else { - optionNames.push(...alias); - } - } - } - - for (const err of commandLine.errors) { - // Ignore errors caused by tstl specific compiler options - if (err.code === tsInvalidCompilerOptionErrorCode) { - let ignore = false; - for (const optionName of optionNames) { - if (err.messageText.toString().indexOf(optionName) !== -1) { - ignore = true; - break; - } - } - - if (!ignore) { - return { isValid: false, errorMessage: `error TS${err.code}: ${err.messageText}`}; - } - } - } - } - - return { isValid: true, result: true }; -} - -/** Find configFile, function from ts api seems to be broken? */ -export function findConfigFile(options: ts.CompilerOptions): ParseResult { - if (!options.project) { - return { isValid: false, errorMessage: `error no base path provided, could not find config.`}; - } - let configPath = options.project; - // If the project path is wrapped in double quotes, remove them - if (/^".*"$/.test(configPath)) { - configPath = configPath.substring(1, configPath.length - 1); - } - /* istanbul ignore if: Testing else part is not really possible via automated tests */ - if (!path.isAbsolute(configPath)) { - // TODO check if options.project can even contain non absolute paths - configPath = path.join(process.cwd(), configPath); - } - if (fs.statSync(configPath).isDirectory()) { - configPath = path.join(configPath, "tsconfig.json"); - } else if (fs.statSync(configPath).isFile() && path.extname(configPath) === ".ts") { - // Search for tsconfig upwards in directory hierarchy starting from the file path - const dir = path.dirname(configPath).split(path.sep); - for (let i = dir.length; i > 0; i--) { - const searchPath = dir.slice(0, i).join("/") + path.sep + "tsconfig.json"; - - // If tsconfig.json was found, stop searching - if (ts.sys.fileExists(searchPath)) { - configPath = searchPath; - break; - } - } - } - - return { isValid: true, result: configPath }; -} diff --git a/src/Compiler.ts b/src/Compiler.ts deleted file mode 100644 index ff0499cfa..000000000 --- a/src/Compiler.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; - -import * as CommandLineParser from "./CommandLineParser"; -import {CompilerOptions, LuaLibImportKind, LuaTarget} from "./CompilerOptions"; -import {LuaTranspiler} from "./LuaTranspiler"; - -export function compile(argv: string[]): void { - const parseResult = CommandLineParser.parseCommandLine(argv); - - if (parseResult.isValid === true) { - - if (parseResult.result.options.help) { - console.log(CommandLineParser.getHelpString()); - return; - } - - if (parseResult.result.options.version) { - console.log(CommandLineParser.version); - return; - } - - /* istanbul ignore if: tested in test/compiler/watchmode.spec with subproccess */ - if (parseResult.result.options.watch) { - watchWithOptions(parseResult.result.fileNames, parseResult.result.options); - } else { - compileFilesWithOptions(parseResult.result.fileNames, parseResult.result.options); - } - } else { - console.error(`Invalid CLI input: ${parseResult.errorMessage}`); - } -} - -/* istanbul ignore next: tested in test/compiler/watchmode.spec with subproccess */ -export function watchWithOptions(fileNames: string[], options: CompilerOptions): void { - let host: ts.WatchCompilerHost; - let config = false; - if (options.project) { - config = true; - host = ts.createWatchCompilerHost(options.project, options, ts.sys, ts.createSemanticDiagnosticsBuilderProgram); - } else { - host = ts.createWatchCompilerHost(fileNames, options, ts.sys, ts.createSemanticDiagnosticsBuilderProgram); - } - - host.afterProgramCreate = program => { - const transpiler = new LuaTranspiler(program.getProgram()); - - const status = transpiler.emitFilesAndReportErrors(); - const errorDiagnostic: ts.Diagnostic = { - category: undefined, - code: 6194, - file: undefined, - length: 0, - messageText: "Found 0 errors. Watching for file changes.", - start: 0, - }; - if (status !== 0) { - errorDiagnostic.messageText = "Found Errors. Watching for file changes."; - errorDiagnostic.code = 6193; - } - host.onWatchStatusChange(errorDiagnostic, host.getNewLine(), program.getCompilerOptions()); - }; - - if (config) { - ts.createWatchProgram( - host as ts.WatchCompilerHostOfConfigFile - ); - } else { - ts.createWatchProgram( - host as ts.WatchCompilerHostOfFilesAndCompilerOptions - ); - } -} - -export function compileFilesWithOptions(fileNames: string[], options: CompilerOptions): void { - const program = ts.createProgram(fileNames, options); - - const transpiler = new LuaTranspiler(program); - - transpiler.emitFilesAndReportErrors(); -} - -const libCache: {[key: string]: string} = {}; - -const defaultCompilerOptions: CompilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, -}; - -export function createStringCompilerProgram( - input: string, options: CompilerOptions = defaultCompilerOptions, filePath = "file.ts"): ts.Program { - const compilerHost = { - directoryExists: () => true, - fileExists: (fileName): boolean => true, - getCanonicalFileName: fileName => fileName, - getCurrentDirectory: () => "", - getDefaultLibFileName: () => "lib.es6.d.ts", - getDirectories: () => [], - getNewLine: () => "\n", - - getSourceFile: (filename: string) => { - if (filename === filePath) { - return ts.createSourceFile(filename, input, ts.ScriptTarget.Latest, false); - } - if (filename.indexOf(".d.ts") !== -1) { - if (!libCache[filename]) { - const typeScriptDir = path.dirname(require.resolve("typescript")); - const filePath = path.join(typeScriptDir, filename); - if (fs.existsSync(filePath)) { - libCache[filename] = fs.readFileSync(filePath).toString(); - } else { - const pathWithLibPrefix = path.join(typeScriptDir, "lib." + filename); - libCache[filename] = fs.readFileSync(pathWithLibPrefix).toString(); - } - } - return ts.createSourceFile(filename, libCache[filename], ts.ScriptTarget.Latest, false); - } - return undefined; - }, - - readFile: () => "", - - useCaseSensitiveFileNames: () => false, - // Don't write output - writeFile: (name, text, writeByteOrderMark) => undefined, - }; - return ts.createProgram([filePath], options, compilerHost); -} - -export function transpileString( - str: string, - options: CompilerOptions = defaultCompilerOptions, - ignoreDiagnostics = false, - filePath = "file.ts" -): string { - const program = createStringCompilerProgram(str, options, filePath); - - if (!ignoreDiagnostics) { - const diagnostics = ts.getPreEmitDiagnostics(program); - const typeScriptErrors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); - - if (typeScriptErrors.length > 0) { - typeScriptErrors.forEach(e => console.warn(e.messageText)); - throw new Error("Encountered invalid TypeScript."); - } - } - - const transpiler = new LuaTranspiler(program); - - const result = transpiler.transpileSourceFile(program.getSourceFile(filePath)); - - return result.trim(); -} diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 24fae4374..47ada916e 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -1,22 +1,107 @@ import * as ts from "typescript"; +import { JsxEmit } from "typescript"; +import * as diagnosticFactories from "./transpilation/diagnostics"; +import { Plugin } from "./transpilation/plugins"; -export interface CompilerOptions extends ts.CompilerOptions { - noHeader?: boolean; +type OmitIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; +}; + +export interface TransformerImport { + transform: string; + import?: string; + after?: boolean; + afterDeclarations?: boolean; + type?: "program" | "config" | "checker" | "raw" | "compilerOptions"; + + [option: string]: any; +} + +export interface LuaPluginImport { + name: string; + import?: string; + + [option: string]: any; +} + +export interface InMemoryLuaPlugin { + plugin: Plugin | ((options: Record) => Plugin); + [option: string]: any; +} + +export interface TypeScriptToLuaOptions { + buildMode?: BuildMode; + extension?: string; + luaBundle?: string; + luaBundleEntry?: string; luaTarget?: LuaTarget; luaLibImport?: LuaLibImportKind; - noHoisting?: boolean; + luaPlugins?: Array; + noImplicitGlobalVariables?: boolean; + noImplicitSelf?: boolean; + noHeader?: boolean; + noResolvePaths?: string[]; + plugins?: Array; + sourceMapTraceback?: boolean; + tstlVerbose?: boolean; + lua51AllowTryCatchInAsyncAwait?: boolean; + measurePerformance?: boolean; } +export type CompilerOptions = OmitIndexSignature & + TypeScriptToLuaOptions & { + [option: string]: any; + }; + export enum LuaLibImportKind { None = "none", - Always = "always", Inline = "inline", Require = "require", + RequireMinimal = "require-minimal", } export enum LuaTarget { + Universal = "universal", + Lua50 = "5.0", Lua51 = "5.1", Lua52 = "5.2", Lua53 = "5.3", - LuaJIT = "jit", + Lua54 = "5.4", + Lua55 = "5.5", + LuaJIT = "JIT", + Luau = "Luau", +} + +export enum BuildMode { + Default = "default", + Library = "library", +} + +export const isBundleEnabled = (options: CompilerOptions) => + options.luaBundle !== undefined && options.luaBundleEntry !== undefined; + +export function validateOptions(options: CompilerOptions): ts.Diagnostic[] { + const diagnostics: ts.Diagnostic[] = []; + + if (options.luaBundle && !options.luaBundleEntry) { + diagnostics.push(diagnosticFactories.luaBundleEntryIsRequired()); + } + + if (options.luaBundle && options.luaLibImport === LuaLibImportKind.Inline) { + diagnostics.push(diagnosticFactories.usingLuaBundleWithInlineMightGenerateDuplicateCode()); + } + + if (options.luaBundle && options.buildMode === BuildMode.Library) { + diagnostics.push(diagnosticFactories.cannotBundleLibrary()); + } + + if (options.jsx && options.jsx !== JsxEmit.React) { + diagnostics.push(diagnosticFactories.unsupportedJsxEmit()); + } + + if (options.paths && !options.baseUrl) { + diagnostics.push(diagnosticFactories.pathsWithoutBaseUrl()); + } + + return diagnostics; } diff --git a/src/Decorator.ts b/src/Decorator.ts deleted file mode 100644 index bf113ee71..000000000 --- a/src/Decorator.ts +++ /dev/null @@ -1,50 +0,0 @@ -export class Decorator { - public static isValid(decoratorKindString: string): boolean { - return this.getDecoratorKind(decoratorKindString) !== undefined; - } - - public static getDecoratorKind(decoratorKindString: string): DecoratorKind { - switch (decoratorKindString.toLowerCase()) { - case "extension": - return DecoratorKind.Extension; - case "metaextension": - return DecoratorKind.MetaExtension; - case "customconstructor": - return DecoratorKind.CustomConstructor; - case "compilemembersonly": - return DecoratorKind.CompileMembersOnly; - case "pureabstract": - return DecoratorKind.PureAbstract; - case "phantom": - return DecoratorKind.Phantom; - case "tuplereturn": - return DecoratorKind.TupleReturn; - case "noclassor": - return DecoratorKind.NoClassOr; - case "luaiterator": - return DecoratorKind.LuaIterator; - } - - return undefined; - } - - public kind: DecoratorKind; - public args: string[]; - - constructor(name: string, args: string[]) { - this.kind = Decorator.getDecoratorKind(name); - this.args = args; - } -} - -export enum DecoratorKind { - Extension = "Extension", - MetaExtension = "MetaExtension", - CustomConstructor = "CustomConstructor", - CompileMembersOnly = "CompileMembersOnly", - PureAbstract = "PureAbstract", - Phantom = "Phantom", - TupleReturn = "TupleReturn", - NoClassOr = "NoClassOr", - LuaIterator = "LuaIterator", -} diff --git a/src/LuaAST.ts b/src/LuaAST.ts index a973354ca..ad4a7e06b 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -1,13 +1,17 @@ // Simplified Lua AST based roughly on http://lua-users.org/wiki/MetaLuaAbstractSyntaxTree, -// https://www.lua.org/manual/5.3/manual.html (9 – The Complete Syntax of Lua) and the TS AST implementation +// https://www.lua.org/manual/5.3/manual.html#9 and the TS AST implementation -// We can ellide a lot of nodes especially tokens and keyowords -// becasue we dont create the AST from text +// We can elide a lot of nodes especially tokens and keywords +// because we don't create the AST from text import * as ts from "typescript"; +import { LuaLibFeature } from "./LuaLib"; +import { castArray } from "./utils"; export enum SyntaxKind { + File, Block, + // Statements DoStatement, VariableDeclarationStatement, @@ -21,12 +25,15 @@ export enum SyntaxKind { LabelStatement, ReturnStatement, BreakStatement, + ContinueStatement, // Luau only. ExpressionStatement, + // Expression StringLiteral, NumericLiteral, NilKeyword, DotsKeyword, + ArgKeyword, TrueKeyword, FalseKeyword, FunctionExpression, @@ -34,155 +41,228 @@ export enum SyntaxKind { TableExpression, UnaryExpression, BinaryExpression, - ParenthesizedExpression, CallExpression, MethodCallExpression, Identifier, TableIndexExpression, + ParenthesizedExpression, + ConditionalExpression, // Luau only + // Operators + // Arithmetic - AdditionOperator, // Maybe use abreviations for those add, sub, mul ... - SubractionOperator, + AdditionOperator, // Maybe use abbreviations for those add, sub, mul ... + SubtractionOperator, MultiplicationOperator, DivisionOperator, FloorDivisionOperator, ModuloOperator, PowerOperator, - NegationOperator, // Unaray minus + NegationOperator, // Unary minus + // Concat ConcatOperator, + // Length - LengthOperator, // Unary + LengthOperator, // Unary + // Relational Ops EqualityOperator, InequalityOperator, LessThanOperator, LessEqualOperator, - GreaterThanOperator, // Syntax Sugar `x > y` <=> `not (y <= x)` - // but we should probably use them to make the output code more readable - GreaterEqualOperator, // Syntax Sugar `x >= y` <=> `not (y < x)` + // Syntax Sugar `x > y` <=> `not (y <= x)` + // but we should probably use them to make the output code more readable + GreaterThanOperator, + GreaterEqualOperator, // Syntax Sugar `x >= y` <=> `not (y < x)` + // Logical AndOperator, OrOperator, - NotOperator, // Unary + NotOperator, // Unary + // Bitwise BitwiseAndOperator, BitwiseOrOperator, BitwiseExclusiveOrOperator, BitwiseRightShiftOperator, - BitwiseArithmeticRightShift, BitwiseLeftShiftOperator, - BitwiseNotOperator, // Unary + BitwiseNotOperator, // Unary } // TODO maybe name this PrefixUnary? not sure it makes sense to do so, because all unary ops in Lua are prefix export type UnaryBitwiseOperator = SyntaxKind.BitwiseNotOperator; -export type UnaryOperator = SyntaxKind.NegationOperator +export type UnaryOperator = + | SyntaxKind.NegationOperator | SyntaxKind.LengthOperator | SyntaxKind.NotOperator | UnaryBitwiseOperator; -export type BinaryBitwiseOperator = SyntaxKind.BitwiseAndOperator | SyntaxKind.BitwiseOrOperator - | SyntaxKind.BitwiseExclusiveOrOperator | SyntaxKind.BitwiseRightShiftOperator - | SyntaxKind.BitwiseArithmeticRightShift | SyntaxKind.BitwiseLeftShiftOperator; +export type BinaryBitwiseOperator = + | SyntaxKind.BitwiseAndOperator + | SyntaxKind.BitwiseOrOperator + | SyntaxKind.BitwiseExclusiveOrOperator + | SyntaxKind.BitwiseRightShiftOperator + | SyntaxKind.BitwiseLeftShiftOperator; export type BinaryOperator = - SyntaxKind.AdditionOperator | SyntaxKind.SubractionOperator | SyntaxKind.MultiplicationOperator - | SyntaxKind.DivisionOperator | SyntaxKind.FloorDivisionOperator | SyntaxKind.ModuloOperator - | SyntaxKind.PowerOperator | SyntaxKind.ConcatOperator | SyntaxKind.EqualityOperator - | SyntaxKind.InequalityOperator | SyntaxKind.LessThanOperator | SyntaxKind.LessEqualOperator - | SyntaxKind.GreaterThanOperator | SyntaxKind.GreaterEqualOperator | SyntaxKind.AndOperator - | SyntaxKind.OrOperator | BinaryBitwiseOperator; + | SyntaxKind.AdditionOperator + | SyntaxKind.SubtractionOperator + | SyntaxKind.MultiplicationOperator + | SyntaxKind.DivisionOperator + | SyntaxKind.FloorDivisionOperator + | SyntaxKind.ModuloOperator + | SyntaxKind.PowerOperator + | SyntaxKind.ConcatOperator + | SyntaxKind.EqualityOperator + | SyntaxKind.InequalityOperator + | SyntaxKind.LessThanOperator + | SyntaxKind.LessEqualOperator + | SyntaxKind.GreaterThanOperator + | SyntaxKind.GreaterEqualOperator + | SyntaxKind.AndOperator + | SyntaxKind.OrOperator + | BinaryBitwiseOperator; export type Operator = UnaryOperator | BinaryOperator; -export type SymbolId = number; +export type SymbolId = number & { _symbolIdBrand: any }; + +export enum NodeFlags { + None = 0, + Inline = 1 << 0, // Keep function body on same line + Declaration = 1 << 1, // Prefer declaration syntax `function foo()` over assignment syntax `foo = function()` + TableUnpackCall = 1 << 2, // This is a table.unpack call +} -// TODO For future sourcemap support? export interface TextRange { - pos: number; - end: number; + line?: number; + column?: number; } export interface Node extends TextRange { kind: SyntaxKind; - parent?: Node; + flags: NodeFlags; } -export function createNode(kind: SyntaxKind, tsOriginal?: ts.Node, parent?: Node): Node { - let pos = -1; - let end = -1; - if (tsOriginal) { - pos = tsOriginal.pos; - end = tsOriginal.end; +export function createNode(kind: SyntaxKind, tsOriginal?: ts.Node): Node { + if (tsOriginal === undefined) { + return { kind, flags: NodeFlags.None }; + } + + const sourcePosition = getSourcePosition(tsOriginal); + if (sourcePosition) { + return { kind, line: sourcePosition.line, column: sourcePosition.column, flags: NodeFlags.None }; + } else { + return { kind, flags: NodeFlags.None }; } - return {kind, parent, pos, end}; } export function cloneNode(node: T): T { - return Object.assign({}, node); + return { ...node }; } -export function setNodeOriginal(node: T, tsOriginal: ts.Node): T { - node.pos = tsOriginal.pos; - node.end = tsOriginal.end; +export function setNodePosition(node: T, position: TextRange): T { + node.line = position.line; + node.column = position.column; + return node; } -export function setParent(node: Node | Node[] | undefined, parent: Node): void { - if (!node) { - return; +export function setNodeOriginal(node: T, tsOriginal: ts.Node): T; +export function setNodeOriginal(node: T | undefined, tsOriginal: ts.Node): T | undefined; +export function setNodeOriginal(node: T | undefined, tsOriginal: ts.Node): T | undefined { + if (node === undefined) { + return undefined; } - if (Array.isArray(node)) { - node.forEach(n => { - n.parent = parent; - if (n.pos === -1 || n.end === -1) { - n.pos = parent.pos; - n.end = parent.end; - } - }); - } else { - node.parent = parent; - if (node.pos === -1 || node.end === -1) { - node.pos = parent.pos; - node.end = parent.end; - } + + const sourcePosition = getSourcePosition(tsOriginal); + if (sourcePosition) { + setNodePosition(node, sourcePosition); + } + + return node; +} + +function getSourcePosition(sourceNode: ts.Node): TextRange | undefined { + const parseTreeNode = ts.getParseTreeNode(sourceNode) ?? sourceNode; + const sourceFile = parseTreeNode.getSourceFile(); + if (sourceFile !== undefined && parseTreeNode.pos >= 0) { + const { line, character } = ts.getLineAndCharacterOfPosition( + sourceFile, + parseTreeNode.pos + parseTreeNode.getLeadingTriviaWidth() + ); + + return { line, column: character }; } } +export function getOriginalPos(node: Node): TextRange { + return { line: node.line, column: node.column }; +} + +export function setNodeFlags(node: T, flags: NodeFlags): T { + node.flags = flags; + return node; +} + +export interface File extends Node { + kind: SyntaxKind.File; + statements: Statement[]; + luaLibFeatures: Set; + trivia: string; +} + +export function isFile(node: Node): node is File { + return node.kind === SyntaxKind.File; +} + +export function createFile( + statements: Statement[], + luaLibFeatures: Set, + trivia: string, + tsOriginal?: ts.Node +): File { + const file = createNode(SyntaxKind.File, tsOriginal) as File; + file.statements = statements; + file.luaLibFeatures = luaLibFeatures; + file.trivia = trivia; + return file; +} + export interface Block extends Node { kind: SyntaxKind.Block; - statements?: Statement[]; + statements: Statement[]; } export function isBlock(node: Node): node is Block { return node.kind === SyntaxKind.Block; } -export function createBlock(statements?: Statement[], tsOriginal?: ts.Node, parent?: Node): Block { - const block = createNode(SyntaxKind.Block, tsOriginal, parent) as Block; - setParent(statements, block); +export function createBlock(statements: Statement[], tsOriginal?: ts.Node): Block { + const block = createNode(SyntaxKind.Block, tsOriginal) as Block; block.statements = statements; return block; } export interface Statement extends Node { _statementBrand: any; + leadingComments?: Array; + trailingComments?: Array; } export interface DoStatement extends Statement { kind: SyntaxKind.DoStatement; - statements?: Statement[]; + statements: Statement[]; } export function isDoStatement(node: Node): node is DoStatement { return node.kind === SyntaxKind.DoStatement; } -export function createDoStatement(statements?: Statement[], tsOriginal?: ts.Node, parent?: Node): DoStatement { - const statement = createNode(SyntaxKind.DoStatement, tsOriginal, parent) as DoStatement; - setParent(statements, statement); +export function createDoStatement(statements: Statement[], tsOriginal?: ts.Node): DoStatement { + const statement = createNode(SyntaxKind.DoStatement, tsOriginal) as DoStatement; statement.statements = statements; return statement; } @@ -201,34 +281,18 @@ export function isVariableDeclarationStatement(node: Node): node is VariableDecl export function createVariableDeclarationStatement( left: Identifier | Identifier[], right?: Expression | Expression[], - tsOriginal?: ts.Node, - parent?: Node -): VariableDeclarationStatement -{ - const statement = createNode( - SyntaxKind.VariableDeclarationStatement, - tsOriginal, - parent - ) as VariableDeclarationStatement; - setParent(left, statement); - if (Array.isArray(left)) { - statement.left = left; - } else { - statement.left = [left]; - } - setParent(right, statement); - if (Array.isArray(right)) { - statement.right = right; - } else if (right) { - statement.right = [right]; - } + tsOriginal?: ts.Node +): VariableDeclarationStatement { + const statement = createNode(SyntaxKind.VariableDeclarationStatement, tsOriginal) as VariableDeclarationStatement; + statement.left = castArray(left); + if (right) statement.right = castArray(right); return statement; } // `test1, test2 = 12, 42` export interface AssignmentStatement extends Statement { kind: SyntaxKind.AssignmentStatement; - left: IdentifierOrTableIndexExpression[]; + left: AssignmentLeftHandSideExpression[]; right: Expression[]; } @@ -237,31 +301,19 @@ export function isAssignmentStatement(node: Node): node is AssignmentStatement { } export function createAssignmentStatement( - left: IdentifierOrTableIndexExpression | IdentifierOrTableIndexExpression[], - right: Expression | Expression[], - tsOriginal?: ts.Node, - parent?: Node -): AssignmentStatement -{ - const statement = createNode(SyntaxKind.AssignmentStatement, tsOriginal, parent) as AssignmentStatement; - setParent(left, statement); - if (Array.isArray(left)) { - statement.left = left; - } else { - statement.left = [left]; - } - setParent(right, statement); - if (Array.isArray(right)) { - statement.right = right; - } else { - statement.right = [right]; - } + left: AssignmentLeftHandSideExpression | AssignmentLeftHandSideExpression[], + right?: Expression | Expression[], + tsOriginal?: ts.Node +): AssignmentStatement { + const statement = createNode(SyntaxKind.AssignmentStatement, tsOriginal) as AssignmentStatement; + statement.left = castArray(left); + statement.right = right ? castArray(right) : []; return statement; } export interface IfStatement extends Statement { kind: SyntaxKind.IfStatement; - condtion: Expression; + condition: Expression; ifBlock: Block; elseBlock?: Block | IfStatement; } @@ -271,19 +323,14 @@ export function isIfStatement(node: Node): node is IfStatement { } export function createIfStatement( - condtion: Expression, + condition: Expression, ifBlock: Block, elseBlock?: Block | IfStatement, - tsOriginal?: ts.Node, - parent?: Node -): IfStatement -{ - const statement = createNode(SyntaxKind.IfStatement, tsOriginal, parent) as IfStatement; - setParent(condtion, statement); - statement.condtion = condtion; - setParent(ifBlock, statement); + tsOriginal?: ts.Node +): IfStatement { + const statement = createNode(SyntaxKind.IfStatement, tsOriginal) as IfStatement; + statement.condition = condition; statement.ifBlock = ifBlock; - setParent(ifBlock, statement); statement.elseBlock = elseBlock; return statement; } @@ -293,55 +340,43 @@ export interface IterationStatement extends Statement { } export function isIterationStatement(node: Node): node is IterationStatement { - return node.kind === SyntaxKind.WhileStatement || node.kind === SyntaxKind.RepeatStatement - || node.kind === SyntaxKind.ForStatement || node.kind === SyntaxKind.ForInStatement; + return ( + node.kind === SyntaxKind.WhileStatement || + node.kind === SyntaxKind.RepeatStatement || + node.kind === SyntaxKind.ForStatement || + node.kind === SyntaxKind.ForInStatement + ); } export interface WhileStatement extends IterationStatement { kind: SyntaxKind.WhileStatement; - condtion: Expression; + condition: Expression; } export function isWhileStatement(node: Node): node is WhileStatement { return node.kind === SyntaxKind.WhileStatement; } -export function createWhileStatement( - body: Block, - condtion: Expression, - tsOriginal?: ts.Node, - parent?: Node -): WhileStatement -{ - const statement = createNode(SyntaxKind.WhileStatement, tsOriginal, parent) as WhileStatement; - setParent(body, statement); +export function createWhileStatement(body: Block, condition: Expression, tsOriginal?: ts.Node): WhileStatement { + const statement = createNode(SyntaxKind.WhileStatement, tsOriginal) as WhileStatement; statement.body = body; - setParent(condtion, statement); - statement.condtion = condtion; + statement.condition = condition; return statement; } export interface RepeatStatement extends IterationStatement { kind: SyntaxKind.RepeatStatement; - condtion: Expression; + condition: Expression; } export function isRepeatStatement(node: Node): node is RepeatStatement { return node.kind === SyntaxKind.RepeatStatement; } -export function createRepeatStatement( - body: Block, - condtion: Expression, - tsOriginal?: ts.Node, - parent?: Node -): RepeatStatement -{ - const statement = createNode(SyntaxKind.RepeatStatement, tsOriginal, parent) as RepeatStatement; - setParent(body, statement); +export function createRepeatStatement(body: Block, condition: Expression, tsOriginal?: ts.Node): RepeatStatement { + const statement = createNode(SyntaxKind.RepeatStatement, tsOriginal) as RepeatStatement; statement.body = body; - setParent(condtion, statement); - statement.condtion = condtion; + statement.condition = condition; return statement; } @@ -364,20 +399,13 @@ export function createForStatement( controlVariableInitializer: Expression, limitExpression: Expression, stepExpression?: Expression, - tsOriginal?: ts.Node, - parent?: Node -): ForStatement -{ - const statement = createNode(SyntaxKind.ForStatement, tsOriginal, parent) as ForStatement; - setParent(body, statement); + tsOriginal?: ts.Node +): ForStatement { + const statement = createNode(SyntaxKind.ForStatement, tsOriginal) as ForStatement; statement.body = body; - setParent(controlVariable, statement); statement.controlVariable = controlVariable; - setParent(controlVariableInitializer, statement); statement.controlVariableInitializer = controlVariableInitializer; - setParent(limitExpression, statement); statement.limitExpression = limitExpression; - setParent(stepExpression, statement); statement.stepExpression = stepExpression; return statement; } @@ -396,67 +424,56 @@ export function createForInStatement( body: Block, names: Identifier[], expressions: Expression[], - tsOriginal?: ts.Node, - parent?: Node -): ForInStatement -{ - const statement = createNode(SyntaxKind.ForInStatement, tsOriginal, parent) as ForInStatement; - setParent(body, statement); + tsOriginal?: ts.Node +): ForInStatement { + const statement = createNode(SyntaxKind.ForInStatement, tsOriginal) as ForInStatement; statement.body = body; - setParent(names, statement); statement.names = names; - setParent(expressions, statement); statement.expressions = expressions; return statement; } export interface GotoStatement extends Statement { kind: SyntaxKind.GotoStatement; - label: string; // or identifier ? + label: string; // or identifier ? } export function isGotoStatement(node: Node): node is GotoStatement { return node.kind === SyntaxKind.GotoStatement; } -export function createGotoStatement(label: string, tsOriginal?: ts.Node, parent?: Node): GotoStatement { - const statement = createNode(SyntaxKind.GotoStatement, tsOriginal, parent) as GotoStatement; +export function createGotoStatement(label: string, tsOriginal?: ts.Node): GotoStatement { + const statement = createNode(SyntaxKind.GotoStatement, tsOriginal) as GotoStatement; statement.label = label; return statement; } export interface LabelStatement extends Statement { kind: SyntaxKind.LabelStatement; - name: string; // or identifier ? + name: string; // or identifier ? } export function isLabelStatement(node: Node): node is LabelStatement { return node.kind === SyntaxKind.LabelStatement; } -export function createLabelStatement(name: string, tsOriginal?: ts.Node, parent?: Node): LabelStatement { - const statement = createNode(SyntaxKind.LabelStatement, tsOriginal, parent) as LabelStatement; +export function createLabelStatement(name: string, tsOriginal?: ts.Node): LabelStatement { + const statement = createNode(SyntaxKind.LabelStatement, tsOriginal) as LabelStatement; statement.name = name; return statement; } export interface ReturnStatement extends Statement { kind: SyntaxKind.ReturnStatement; - expressions?: Expression[]; + expressions: Expression[]; } export function isReturnStatement(node: Node): node is ReturnStatement { return node.kind === SyntaxKind.ReturnStatement; } -export function createReturnStatement( - expressions?: Expression[], - tsOriginal?: ts.Node, - parent?: Node -): ReturnStatement -{ - const statement = createNode(SyntaxKind.ReturnStatement, tsOriginal, parent) as ReturnStatement; - setParent(expressions, statement); +export function createReturnStatement(expressions: Expression[], tsOriginal?: ts.Node): ReturnStatement { + const statement = createNode(SyntaxKind.ReturnStatement, tsOriginal) as ReturnStatement; statement.expressions = expressions; return statement; } @@ -469,8 +486,20 @@ export function isBreakStatement(node: Node): node is BreakStatement { return node.kind === SyntaxKind.BreakStatement; } -export function createBreakStatement(tsOriginal?: ts.Node, parent?: Node): BreakStatement { - return createNode(SyntaxKind.BreakStatement, tsOriginal, parent) as BreakStatement; +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 { @@ -482,14 +511,8 @@ export function isExpressionStatement(node: Node): node is ExpressionStatement { return node.kind === SyntaxKind.ExpressionStatement; } -export function createExpressionStatement( - expressions: Expression, - tsOriginal?: ts.Node, - parent?: Node -): ExpressionStatement -{ - const statement = createNode(SyntaxKind.ExpressionStatement, tsOriginal, parent) as ExpressionStatement; - setParent(expressions, statement); +export function createExpressionStatement(expressions: Expression, tsOriginal?: ts.Node): ExpressionStatement { + const statement = createNode(SyntaxKind.ExpressionStatement, tsOriginal) as ExpressionStatement; statement.expression = expressions; return statement; } @@ -508,8 +531,8 @@ export function isNilLiteral(node: Node): node is NilLiteral { return node.kind === SyntaxKind.NilKeyword; } -export function createNilLiteral(tsOriginal?: ts.Node, parent?: Node): NilLiteral { - return createNode(SyntaxKind.NilKeyword, tsOriginal, parent) as NilLiteral; +export function createNilLiteral(tsOriginal?: ts.Node): NilLiteral { + return createNode(SyntaxKind.NilKeyword, tsOriginal) as NilLiteral; } export interface BooleanLiteral extends Expression { @@ -520,12 +543,8 @@ export function isBooleanLiteral(node: Node): node is BooleanLiteral { return node.kind === SyntaxKind.TrueKeyword || node.kind === SyntaxKind.FalseKeyword; } -export function createBooleanLiteral(value: boolean, tsOriginal?: ts.Node, parent?: Node): BooleanLiteral { - if (value) { - return createNode(SyntaxKind.TrueKeyword, tsOriginal, parent) as BooleanLiteral; - } else { - return createNode(SyntaxKind.FalseKeyword, tsOriginal, parent) as BooleanLiteral; - } +export function createBooleanLiteral(value: boolean, tsOriginal?: ts.Node): BooleanLiteral { + return createNode(value ? SyntaxKind.TrueKeyword : SyntaxKind.FalseKeyword, tsOriginal) as BooleanLiteral; } // TODO Call this DotsLiteral or DotsKeyword? @@ -537,13 +556,25 @@ export function isDotsLiteral(node: Node): node is DotsLiteral { return node.kind === SyntaxKind.DotsKeyword; } -export function createDotsLiteral(tsOriginal?: ts.Node, parent?: Node): DotsLiteral { - return createNode(SyntaxKind.DotsKeyword, tsOriginal, parent) as DotsLiteral; +export function createDotsLiteral(tsOriginal?: ts.Node): DotsLiteral { + return createNode(SyntaxKind.DotsKeyword, tsOriginal) as DotsLiteral; +} + +export interface ArgLiteral extends Expression { + kind: SyntaxKind.ArgKeyword; +} + +export function isArgLiteral(node: Node): node is ArgLiteral { + return node.kind === SyntaxKind.ArgKeyword; +} + +export function createArgLiteral(tsOriginal?: ts.Node): ArgLiteral { + return createNode(SyntaxKind.ArgKeyword, tsOriginal) as ArgLiteral; } // StringLiteral / NumberLiteral // TODO TS uses the export interface "LiteralLikeNode" with a "text: string" member -// but since we dont parse from text i think we can simplify by just having a value member +// but since we don't parse from text I think we can simplify by just having a value member // TODO NumericLiteral or NumberLiteral? export interface NumericLiteral extends Expression { @@ -555,8 +586,8 @@ export function isNumericLiteral(node: Node): node is NumericLiteral { return node.kind === SyntaxKind.NumericLiteral; } -export function createNumericLiteral(value: number, tsOriginal?: ts.Node, parent?: Node): NumericLiteral { - const expression = createNode(SyntaxKind.NumericLiteral, tsOriginal, parent) as NumericLiteral; +export function createNumericLiteral(value: number, tsOriginal?: ts.Node): NumericLiteral { + const expression = createNode(SyntaxKind.NumericLiteral, tsOriginal) as NumericLiteral; expression.value = value; return expression; } @@ -570,25 +601,29 @@ export function isStringLiteral(node: Node): node is StringLiteral { return node.kind === SyntaxKind.StringLiteral; } -export function createStringLiteral(value: string | ts.__String, tsOriginal?: ts.Node, parent?: Node): StringLiteral { - const expression = createNode(SyntaxKind.StringLiteral, tsOriginal, parent) as StringLiteral; - expression.value = value as string; +export function createStringLiteral(value: string, tsOriginal?: ts.Node): StringLiteral { + const expression = createNode(SyntaxKind.StringLiteral, tsOriginal) as StringLiteral; + expression.value = value; return expression; } -// There is no export function statement/declaration because those are just syntax sugar -// -// `function f () body end` becomes `f = function () body` end -// `function t.a.b.c.f () body end` becomes `t.a.b.c.f = function () body end` -// `local function f () body end` becomes `local f; f = function () body end` NOT `local f = function () body end` -// See https://www.lua.org/manual/5.3/manual.html 3.4.11 -// -// We should probably create helper functions to create the different export function declarations +export function isLiteral( + node: Node +): node is NilLiteral | DotsLiteral | ArgLiteral | BooleanLiteral | NumericLiteral | StringLiteral { + return ( + isNilLiteral(node) || + isDotsLiteral(node) || + isArgLiteral(node) || + isBooleanLiteral(node) || + isNumericLiteral(node) || + isStringLiteral(node) + ); +} + export interface FunctionExpression extends Expression { kind: SyntaxKind.FunctionExpression; params?: Identifier[]; dots?: DotsLiteral; - restParamName?: Identifier; body: Block; } @@ -600,20 +635,14 @@ export function createFunctionExpression( body: Block, params?: Identifier[], dots?: DotsLiteral, - restParamName?: Identifier, - tsOriginal?: ts.Node, - parent?: Node -): FunctionExpression -{ - const expression = createNode(SyntaxKind.FunctionExpression, tsOriginal, parent) as FunctionExpression; - setParent(body, expression); + flags = NodeFlags.None, + tsOriginal?: ts.Node +): FunctionExpression { + const expression = createNode(SyntaxKind.FunctionExpression, tsOriginal) as FunctionExpression; expression.body = body; - setParent(params, expression); expression.params = params; - setParent(dots, expression); expression.dots = dots; - setParent(restParamName, expression); - expression.restParamName = restParamName; + expression.flags = flags; return expression; } @@ -630,35 +659,25 @@ export function isTableFieldExpression(node: Node): node is TableFieldExpression export function createTableFieldExpression( value: Expression, key?: Expression, - tsOriginal?: ts.Node, - parent?: Node -): TableFieldExpression -{ - const expression = createNode(SyntaxKind.TableExpression, tsOriginal, parent) as TableFieldExpression; - setParent(value, expression); + tsOriginal?: ts.Node +): TableFieldExpression { + const expression = createNode(SyntaxKind.TableFieldExpression, tsOriginal) as TableFieldExpression; expression.value = value; - setParent(key, expression); expression.key = key; return expression; } export interface TableExpression extends Expression { kind: SyntaxKind.TableExpression; - fields?: TableFieldExpression[]; + fields: TableFieldExpression[]; } export function isTableExpression(node: Node): node is TableExpression { return node.kind === SyntaxKind.TableExpression; } -export function createTableExpression( - fields?: TableFieldExpression[], - tsOriginal?: ts.Node, - parent?: Node -): TableExpression -{ - const expression = createNode(SyntaxKind.TableExpression, tsOriginal, parent) as TableExpression; - setParent(fields, expression); +export function createTableExpression(fields: TableFieldExpression[] = [], tsOriginal?: ts.Node): TableExpression { + const expression = createNode(SyntaxKind.TableExpression, tsOriginal) as TableExpression; expression.fields = fields; return expression; } @@ -676,12 +695,9 @@ export function isUnaryExpression(node: Node): node is UnaryExpression { export function createUnaryExpression( operand: Expression, operator: UnaryOperator, - tsOriginal?: ts.Node, - parent?: Node -): UnaryExpression -{ - const expression = createNode(SyntaxKind.UnaryExpression, tsOriginal, parent) as UnaryExpression; - setParent(operand, expression); + tsOriginal?: ts.Node +): UnaryExpression { + const expression = createNode(SyntaxKind.UnaryExpression, tsOriginal) as UnaryExpression; expression.operand = operand; expression.operator = operator; return expression; @@ -702,44 +718,19 @@ export function createBinaryExpression( left: Expression, right: Expression, operator: BinaryOperator, - tsOriginal?: ts.Node, - parent?: Node -): BinaryExpression -{ - const expression = createNode(SyntaxKind.BinaryExpression, tsOriginal, parent) as BinaryExpression; - setParent(left, expression); + tsOriginal?: ts.Node +): BinaryExpression { + const expression = createNode(SyntaxKind.BinaryExpression, tsOriginal) as BinaryExpression; expression.left = left; - setParent(right, expression); expression.right = right; expression.operator = operator; return expression; } -export interface ParenthesizedExpression extends Expression { - kind: SyntaxKind.ParenthesizedExpression; - innerEpxression: Expression; -} - -export function isParenthesizedExpression(node: Node): node is ParenthesizedExpression { - return node.kind === SyntaxKind.ParenthesizedExpression; -} - -export function createParenthesizedExpression( - innerExpression: Expression, - tsOriginal?: ts.Node, - parent?: Node -): ParenthesizedExpression -{ - const expression = createNode(SyntaxKind.ParenthesizedExpression, tsOriginal, parent) as ParenthesizedExpression; - setParent(innerExpression, expression); - expression.innerEpxression = innerExpression; - return expression; -} - export interface CallExpression extends Expression { kind: SyntaxKind.CallExpression; expression: Expression; - params?: Expression[]; + params: Expression[]; } export function isCallExpression(node: Node): node is CallExpression { @@ -748,15 +739,11 @@ export function isCallExpression(node: Node): node is CallExpression { export function createCallExpression( expression: Expression, - params?: Expression[], - tsOriginal?: ts.Node, - parent?: Node -): CallExpression -{ - const callExpression = createNode(SyntaxKind.CallExpression, tsOriginal, parent) as CallExpression; - setParent(expression, callExpression); + params: Expression[], + tsOriginal?: ts.Node +): CallExpression { + const callExpression = createNode(SyntaxKind.CallExpression, tsOriginal) as CallExpression; callExpression.expression = expression; - setParent(params, expression); callExpression.params = params; return callExpression; } @@ -765,7 +752,7 @@ export interface MethodCallExpression extends Expression { kind: SyntaxKind.MethodCallExpression; prefixExpression: Expression; name: Identifier; - params?: Expression[]; + params: Expression[]; } export function isMethodCallExpression(node: Node): node is MethodCallExpression { @@ -775,24 +762,21 @@ export function isMethodCallExpression(node: Node): node is MethodCallExpression export function createMethodCallExpression( prefixExpression: Expression, name: Identifier, - params?: Expression[], - tsOriginal?: ts.Node, - parent?: Node -): MethodCallExpression -{ - const callExpression = createNode(SyntaxKind.MethodCallExpression, tsOriginal, parent) as MethodCallExpression; - setParent(prefixExpression, callExpression); + params: Expression[], + tsOriginal?: ts.Node +): MethodCallExpression { + const callExpression = createNode(SyntaxKind.MethodCallExpression, tsOriginal) as MethodCallExpression; callExpression.prefixExpression = prefixExpression; - setParent(name, callExpression); callExpression.name = name; - setParent(params, callExpression); callExpression.params = params; return callExpression; } export interface Identifier extends Expression { kind: SyntaxKind.Identifier; + exportable: boolean; text: string; + originalName?: string; symbolId?: SymbolId; } @@ -801,24 +785,26 @@ export function isIdentifier(node: Node): node is Identifier { } export function createIdentifier( - text: string | ts.__String, + text: string, tsOriginal?: ts.Node, symbolId?: SymbolId, - parent?: Node -): Identifier -{ - const expression = createNode(SyntaxKind.Identifier, tsOriginal, parent) as Identifier; - expression.text = text as string; + originalName?: string +): Identifier { + const expression = createNode(SyntaxKind.Identifier, tsOriginal) as Identifier; + expression.exportable = true; + expression.text = text; expression.symbolId = symbolId; + expression.originalName = originalName; return expression; } -export function cloneIdentifier(identifier: Identifier): Identifier { - return createIdentifier(identifier.text, undefined, identifier.symbolId); +export function cloneIdentifier(identifier: Identifier, tsOriginal?: ts.Node): Identifier { + return createIdentifier(identifier.text, tsOriginal, identifier.symbolId, identifier.originalName); } -export function createAnnonymousIdentifier(tsOriginal?: ts.Node, parent?: Node): Identifier { - const expression = createNode(SyntaxKind.Identifier, tsOriginal, parent) as Identifier; +export function createAnonymousIdentifier(tsOriginal?: ts.Node): Identifier { + const expression = createNode(SyntaxKind.Identifier, tsOriginal) as Identifier; + expression.exportable = false; expression.text = "____"; return expression; } @@ -836,16 +822,79 @@ export function isTableIndexExpression(node: Node): node is TableIndexExpression export function createTableIndexExpression( table: Expression, index: Expression, - tsOriginal?: ts.Node, - parent?: Node -): TableIndexExpression -{ - const expression = createNode(SyntaxKind.TableIndexExpression, tsOriginal, parent) as TableIndexExpression; - setParent(table, expression); + tsOriginal?: ts.Node +): TableIndexExpression { + const expression = createNode(SyntaxKind.TableIndexExpression, tsOriginal) as TableIndexExpression; expression.table = table; - setParent(index, expression); expression.index = index; return expression; } -export type IdentifierOrTableIndexExpression = Identifier | TableIndexExpression; +export type AssignmentLeftHandSideExpression = Identifier | TableIndexExpression; + +export function isAssignmentLeftHandSideExpression(node: Node): node is AssignmentLeftHandSideExpression { + return isIdentifier(node) || isTableIndexExpression(node); +} + +export type FunctionDefinition = (VariableDeclarationStatement | AssignmentStatement) & { + right: [FunctionExpression]; +}; + +export function isFunctionDefinition( + statement: VariableDeclarationStatement | AssignmentStatement +): statement is FunctionDefinition { + return statement.left.length === 1 && statement.right?.length === 1 && isFunctionExpression(statement.right[0]); +} + +export type InlineFunctionExpression = FunctionExpression & { + body: { statements: [ReturnStatement & { expressions: Expression[] }] }; +}; + +export function isInlineFunctionExpression(expression: FunctionExpression): expression is InlineFunctionExpression { + return ( + expression.body.statements?.length === 1 && + isReturnStatement(expression.body.statements[0]) && + expression.body.statements[0].expressions !== undefined && + (expression.flags & NodeFlags.Inline) !== 0 + ); +} + +export type ParenthesizedExpression = Expression & { + expression: Expression; +}; + +export function isParenthesizedExpression(node: Node): node is ParenthesizedExpression { + return node.kind === SyntaxKind.ParenthesizedExpression; +} + +export function createParenthesizedExpression(expression: Expression, tsOriginal?: ts.Node): ParenthesizedExpression { + const parenthesizedExpression = createNode( + SyntaxKind.ParenthesizedExpression, + tsOriginal + ) as ParenthesizedExpression; + 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/LuaLib.ts b/src/LuaLib.ts index 864d438f6..6320bc99c 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -1,74 +1,313 @@ -import * as fs from "fs"; import * as path from "path"; +import { EmitHost } from "./transpilation"; +import * as lua from "./LuaAST"; +import { LuaTarget } from "./CompilerOptions"; +import { getOrUpdate } from "./utils"; export enum LuaLibFeature { + ArrayAt = "ArrayAt", ArrayConcat = "ArrayConcat", + ArrayEntries = "ArrayEntries", ArrayEvery = "ArrayEvery", + ArrayFill = "ArrayFill", ArrayFilter = "ArrayFilter", ArrayForEach = "ArrayForEach", + ArrayFind = "ArrayFind", + ArrayFindIndex = "ArrayFindIndex", + ArrayFrom = "ArrayFrom", + ArrayIncludes = "ArrayIncludes", ArrayIndexOf = "ArrayIndexOf", + ArrayIsArray = "ArrayIsArray", + ArrayJoin = "ArrayJoin", ArrayMap = "ArrayMap", ArrayPush = "ArrayPush", + ArrayPushArray = "ArrayPushArray", + ArrayReduce = "ArrayReduce", + ArrayReduceRight = "ArrayReduceRight", ArrayReverse = "ArrayReverse", - ArrayShift = "ArrayShift", ArrayUnshift = "ArrayUnshift", ArraySort = "ArraySort", ArraySlice = "ArraySlice", ArraySome = "ArraySome", ArraySplice = "ArraySplice", - ClassIndex = "ClassIndex", - ClassNewIndex = "ClassNewIndex", - FunctionApply = "FunctionApply", + ArrayToObject = "ArrayToObject", + ArrayFlat = "ArrayFlat", + ArrayFlatMap = "ArrayFlatMap", + ArraySetLength = "ArraySetLength", + ArrayToReversed = "ArrayToReversed", + ArrayToSorted = "ArrayToSorted", + ArrayToSpliced = "ArrayToSpliced", + ArrayWith = "ArrayWith", + Await = "Await", + Class = "Class", + ClassExtends = "ClassExtends", + CloneDescriptor = "CloneDescriptor", + CountVarargs = "CountVarargs", + Decorate = "Decorate", + DecorateLegacy = "DecorateLegacy", + DecorateParam = "DecorateParam", + Delete = "Delete", + DelegatedYield = "DelegatedYield", + DescriptorGet = "DescriptorGet", + DescriptorSet = "DescriptorSet", + Error = "Error", FunctionBind = "FunctionBind", - FunctionCall = "FunctionCall", - Index = "Index", + Generator = "Generator", InstanceOf = "InstanceOf", + InstanceOfObject = "InstanceOfObject", Iterator = "Iterator", + LuaIteratorSpread = "LuaIteratorSpread", Map = "Map", - NewIndex = "NewIndex", + MapGroupBy = "MapGroupBy", + Match = "Match", + MathAtan2 = "MathAtan2", + MathModf = "MathModf", + MathSign = "MathSign", + MathTrunc = "MathTrunc", + New = "New", + Number = "Number", + NumberIsFinite = "NumberIsFinite", + NumberIsInteger = "NumberIsInteger", + NumberIsNaN = "NumberIsNaN", + NumberParseInt = "ParseInt", + NumberParseFloat = "ParseFloat", + NumberToString = "NumberToString", + NumberToFixed = "NumberToFixed", ObjectAssign = "ObjectAssign", + ObjectDefineProperty = "ObjectDefineProperty", ObjectEntries = "ObjectEntries", + ObjectFromEntries = "ObjectFromEntries", + ObjectGetOwnPropertyDescriptor = "ObjectGetOwnPropertyDescriptor", + ObjectGetOwnPropertyDescriptors = "ObjectGetOwnPropertyDescriptors", + ObjectGroupBy = "ObjectGroupBy", ObjectKeys = "ObjectKeys", + ObjectRest = "ObjectRest", ObjectValues = "ObjectValues", + ParseFloat = "ParseFloat", + ParseInt = "ParseInt", + Promise = "Promise", + PromiseAll = "PromiseAll", + PromiseAllSettled = "PromiseAllSettled", + PromiseAny = "PromiseAny", + PromiseRace = "PromiseRace", Set = "Set", + SetDescriptor = "SetDescriptor", + SparseArrayNew = "SparseArrayNew", + SparseArrayPush = "SparseArrayPush", + SparseArraySpread = "SparseArraySpread", WeakMap = "WeakMap", WeakSet = "WeakSet", + SourceMapTraceBack = "SourceMapTraceBack", + Spread = "Spread", + StringAccess = "StringAccess", + StringCharAt = "StringCharAt", + StringCharCodeAt = "StringCharCodeAt", + StringEndsWith = "StringEndsWith", + StringIncludes = "StringIncludes", + StringPadEnd = "StringPadEnd", + StringPadStart = "StringPadStart", StringReplace = "StringReplace", + StringReplaceAll = "StringReplaceAll", + StringSlice = "StringSlice", StringSplit = "StringSplit", - StringConcat = "StringConcat", + StringStartsWith = "StringStartsWith", + StringSubstr = "StringSubstr", + StringSubstring = "StringSubstring", + StringTrim = "StringTrim", + StringTrimEnd = "StringTrimEnd", + StringTrimStart = "StringTrimStart", Symbol = "Symbol", SymbolRegistry = "SymbolRegistry", + TypeOf = "TypeOf", + Unpack = "Unpack", + Using = "Using", + UsingAsync = "UsingAsync", } -const luaLibDependencies: {[lib in LuaLibFeature]?: LuaLibFeature[]} = { - Iterator: [LuaLibFeature.Symbol], - Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], - Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], - WeakMap: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], - WeakSet: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol], - SymbolRegistry: [LuaLibFeature.Symbol], -}; - -export class LuaLib { - public static loadFeatures(features: Iterable): string { - let result = ""; - - const loadedFeatures = new Set(); - - function load(feature: LuaLibFeature): void { - if (!loadedFeatures.has(feature)) { - loadedFeatures.add(feature); - if (luaLibDependencies[feature]) { - luaLibDependencies[feature].forEach(load); - } - const featureFile = path.resolve(__dirname, `../dist/lualib/${feature}.lua`); - result += fs.readFileSync(featureFile).toString() + "\n"; +export interface LuaLibFeatureInfo { + dependencies?: LuaLibFeature[]; + exports: string[]; +} + +export type LuaLibModulesInfo = Record; + +export function resolveLuaLibDir(luaTarget: LuaTarget) { + const luaLibDir = luaTarget === LuaTarget.Lua50 ? "5.0" : "universal"; + return path.resolve(__dirname, path.join("..", "dist", "lualib", luaLibDir)); +} + +export const luaLibModulesInfoFileName = "lualib_module_info.json"; +const luaLibModulesInfo = new Map(); + +export function getLuaLibModulesInfo(luaTarget: LuaTarget, emitHost: EmitHost): LuaLibModulesInfo { + if (!luaLibModulesInfo.has(luaTarget)) { + const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName); + const result = emitHost.readFile(lualibPath); + if (result !== undefined) { + luaLibModulesInfo.set(luaTarget, JSON.parse(result) as LuaLibModulesInfo); + } else { + throw new Error(`Could not load lualib dependencies from '${lualibPath}'`); + } + } + return luaLibModulesInfo.get(luaTarget)!; +} + +// This caches the names of lualib exports to their LuaLibFeature, avoiding a linear search for every lookup +const lualibExportToFeature = new Map>(); + +export function getLuaLibExportToFeatureMap( + luaTarget: LuaTarget, + emitHost: EmitHost +): ReadonlyMap { + if (!lualibExportToFeature.has(luaTarget)) { + const luaLibModulesInfo = getLuaLibModulesInfo(luaTarget, emitHost); + const map = new Map(); + for (const [feature, info] of Object.entries(luaLibModulesInfo)) { + for (const exportName of info.exports) { + map.set(exportName, feature as LuaLibFeature); } } + lualibExportToFeature.set(luaTarget, map); + } + + return lualibExportToFeature.get(luaTarget)!; +} + +const lualibFeatureCache = new Map>(); + +export function readLuaLibFeature(feature: LuaLibFeature, luaTarget: LuaTarget, emitHost: EmitHost): string { + const featureMap = getOrUpdate(lualibFeatureCache, luaTarget, () => new Map()); + if (!featureMap.has(feature)) { + const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`); + const luaLibFeature = emitHost.readFile(featurePath); + if (luaLibFeature === undefined) { + throw new Error(`Could not load lualib feature from '${featurePath}'`); + } + featureMap.set(feature, luaLibFeature); + } + return featureMap.get(feature)!; +} + +export function resolveRecursiveLualibFeatures( + features: Iterable, + luaTarget: LuaTarget, + emitHost: EmitHost, + luaLibModulesInfo: LuaLibModulesInfo = getLuaLibModulesInfo(luaTarget, emitHost) +): LuaLibFeature[] { + const loadedFeatures = new Set(); + const result: LuaLibFeature[] = []; - for (const feature of features) { - load(feature); + function load(feature: LuaLibFeature): void { + if (loadedFeatures.has(feature)) return; + loadedFeatures.add(feature); + + const dependencies = luaLibModulesInfo[feature]?.dependencies; + if (dependencies) { + dependencies.forEach(load); } - return result; + + result.push(feature); + } + + for (const feature of features) { + load(feature); } + + return result; +} + +export function loadInlineLualibFeatures( + features: Iterable, + luaTarget: LuaTarget, + emitHost: EmitHost +): string { + return resolveRecursiveLualibFeatures(features, luaTarget, emitHost) + .map(feature => readLuaLibFeature(feature, luaTarget, emitHost)) + .join("\n"); +} + +export function loadImportedLualibFeatures( + features: Iterable, + luaTarget: LuaTarget, + emitHost: EmitHost +): lua.Statement[] { + const luaLibModuleInfo = getLuaLibModulesInfo(luaTarget, emitHost); + + const imports = Array.from(features).flatMap(feature => luaLibModuleInfo[feature].exports); + if (imports.length === 0) { + return []; + } + + const requireCall = lua.createCallExpression(lua.createIdentifier("require"), [ + lua.createStringLiteral("lualib_bundle"), + ]); + + const luaLibId = lua.createIdentifier("____lualib"); + const importStatement = lua.createVariableDeclarationStatement(luaLibId, requireCall); + const statements: lua.Statement[] = [importStatement]; + // local = ____luaLib. + for (const item of imports) { + statements.push( + lua.createVariableDeclarationStatement( + lua.createIdentifier(item), + lua.createTableIndexExpression(luaLibId, lua.createStringLiteral(item)) + ) + ); + } + return statements; +} + +const luaLibBundleContent = new Map(); + +export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): string { + const lualibPath = path.join(resolveLuaLibDir(luaTarget), "lualib_bundle.lua"); + if (!luaLibBundleContent.has(lualibPath)) { + const result = emitHost.readFile(lualibPath); + if (result !== undefined) { + luaLibBundleContent.set(lualibPath, result); + } else { + throw new Error(`Could not load lualib bundle from '${lualibPath}'`); + } + } + + return luaLibBundleContent.get(lualibPath) as string; +} + +export function getLualibBundleReturn(exportedValues: string[]): string { + return `\nreturn {\n${exportedValues.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`; +} + +export function buildMinimalLualibBundle( + features: Iterable, + luaTarget: LuaTarget, + emitHost: EmitHost +): string { + const code = loadInlineLualibFeatures(features, luaTarget, emitHost); + const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost); + const exports = Array.from(features).flatMap(feature => moduleInfo[feature].exports); + + return code + getLualibBundleReturn(exports); +} + +export function findUsedLualibFeatures( + luaTarget: LuaTarget, + emitHost: EmitHost, + luaContents: string[] +): Set { + const features = new Set(); + const exportToFeatureMap = getLuaLibExportToFeatureMap(luaTarget, emitHost); + + for (const lua of luaContents) { + const regex = /^local (\w+) = ____lualib\.(\w+)$/gm; + while (true) { + const match = regex.exec(lua); + if (!match) break; + const [, localName, exportName] = match; + if (localName !== exportName) continue; + const feature = exportToFeatureMap.get(exportName); + if (feature) features.add(feature); + } + } + + return features; } diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 0f509c593..b0a02dfaf 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -1,411 +1,948 @@ -import * as tstl from "./LuaAST"; +import * as path from "path"; +import { Mapping, SourceMapGenerator, SourceNode } from "source-map"; +import * as ts from "typescript"; +import { CompilerOptions, isBundleEnabled, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; +import * as lua from "./LuaAST"; +import { loadImportedLualibFeatures, loadInlineLualibFeatures, LuaLibFeature } from "./LuaLib"; +import { isValidLuaIdentifier, shouldAllowUnicode } from "./transformation/utils/safe-names"; +import { EmitHost, getEmitPath } from "./transpilation"; +import { intersperse, normalizeSlashes } from "./utils"; + +// https://www.lua.org/pil/2.4.html +// https://www.ecma-international.org/ecma-262/10.0/index.html#table-34 +const escapeStringRegExp = /[\b\f\n\r\t\v\\"\0]/g; +const escapeStringMap: Record = { + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\v": "\\v", + "\\": "\\\\", + '"': '\\"', + "\0": "\\0", +}; + +export const escapeString = (value: string) => `"${value.replace(escapeStringRegExp, char => escapeStringMap[char])}"`; + +export const tstlHeader = "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n"; + +/** + * Checks that a name is valid for use in lua function declaration syntax: + * + * `foo.bar` => passes (`function foo.bar()` is valid) + * `getFoo().bar` => fails (`function getFoo().bar()` would be illegal) + */ +const isValidLuaFunctionDeclarationName = (str: string, options: CompilerOptions) => + (shouldAllowUnicode(options) ? /^[a-zA-Z0-9_\u00FF-\uFFFD.]+$/ : /^[a-zA-Z0-9_.]+$/).test(str); + +/** + * Returns true if expression contains no function calls. + */ +function isSimpleExpression(expression: lua.Expression): boolean { + switch (expression.kind) { + case lua.SyntaxKind.CallExpression: + case lua.SyntaxKind.MethodCallExpression: + case lua.SyntaxKind.FunctionExpression: + return false; + + case lua.SyntaxKind.TableExpression: + const tableExpression = expression as lua.TableExpression; + return tableExpression.fields.every(e => isSimpleExpression(e)); + + case lua.SyntaxKind.TableFieldExpression: + const fieldExpression = expression as lua.TableFieldExpression; + return ( + (!fieldExpression.key || isSimpleExpression(fieldExpression.key)) && + isSimpleExpression(fieldExpression.value) + ); + + case lua.SyntaxKind.TableIndexExpression: + const indexExpression = expression as lua.TableIndexExpression; + return isSimpleExpression(indexExpression.table) && isSimpleExpression(indexExpression.index); + + case lua.SyntaxKind.UnaryExpression: + return isSimpleExpression((expression as lua.UnaryExpression).operand); + + case lua.SyntaxKind.BinaryExpression: + const binaryExpression = expression as lua.BinaryExpression; + return isSimpleExpression(binaryExpression.left) && isSimpleExpression(binaryExpression.right); + } + + return true; +} + +type SourceChunk = string | SourceNode; + +export type Printer = (program: ts.Program, emitHost: EmitHost, fileName: string, file: lua.File) => PrintResult; -import { TSHelper as tsHelper } from "./TSHelper"; -import { LuaLibFeature, LuaLib } from "./LuaLib"; -import { CompilerOptions } from "./CompilerOptions"; -import { LuaLibImportKind } from "./CompilerOptions"; +export interface PrintResult { + code: string; + sourceMap: string; + sourceMapNode: SourceNode; +} + +export function createPrinter(printers: Printer[]): Printer { + if (printers.length === 0) { + return (program, emitHost, fileName, file) => new LuaPrinter(emitHost, program, fileName).print(file); + } else if (printers.length === 1) { + return printers[0]; + } else { + throw new Error("Only one plugin can specify 'printer'"); + } +} export class LuaPrinter { - /* tslint:disable:object-literal-sort-keys */ - private static operatorMap: {[key in tstl.Operator]: string} = { - [tstl.SyntaxKind.AdditionOperator]: "+", - [tstl.SyntaxKind.SubractionOperator]: "-", - [tstl.SyntaxKind.MultiplicationOperator]: "*", - [tstl.SyntaxKind.DivisionOperator]: "/", - [tstl.SyntaxKind.FloorDivisionOperator]: "//", - [tstl.SyntaxKind.ModuloOperator]: "%", - [tstl.SyntaxKind.PowerOperator]: "^", - [tstl.SyntaxKind.NegationOperator]: "-", - [tstl.SyntaxKind.ConcatOperator]: "..", - [tstl.SyntaxKind.LengthOperator]: "#", - [tstl.SyntaxKind.EqualityOperator]: "==", - [tstl.SyntaxKind.InequalityOperator]: "~=", - [tstl.SyntaxKind.LessThanOperator]: "<", - [tstl.SyntaxKind.LessEqualOperator]: "<=", - [tstl.SyntaxKind.GreaterThanOperator]: ">", - [tstl.SyntaxKind.GreaterEqualOperator]: ">=", - [tstl.SyntaxKind.AndOperator]: "and", - [tstl.SyntaxKind.OrOperator]: "or", - [tstl.SyntaxKind.NotOperator]: "not ", - [tstl.SyntaxKind.BitwiseAndOperator]: "&", - [tstl.SyntaxKind.BitwiseOrOperator]: "|", - [tstl.SyntaxKind.BitwiseExclusiveOrOperator]: "~", - [tstl.SyntaxKind.BitwiseRightShiftOperator]: ">>", - [tstl.SyntaxKind.BitwiseArithmeticRightShift]: ">>>", - [tstl.SyntaxKind.BitwiseLeftShiftOperator]: "<<", - [tstl.SyntaxKind.BitwiseNotOperator]: "~", + private static operatorMap: Record = { + [lua.SyntaxKind.AdditionOperator]: "+", + [lua.SyntaxKind.SubtractionOperator]: "-", + [lua.SyntaxKind.MultiplicationOperator]: "*", + [lua.SyntaxKind.DivisionOperator]: "/", + [lua.SyntaxKind.FloorDivisionOperator]: "//", + [lua.SyntaxKind.ModuloOperator]: "%", + [lua.SyntaxKind.PowerOperator]: "^", + [lua.SyntaxKind.NegationOperator]: "-", + [lua.SyntaxKind.ConcatOperator]: "..", + [lua.SyntaxKind.LengthOperator]: "#", + [lua.SyntaxKind.EqualityOperator]: "==", + [lua.SyntaxKind.InequalityOperator]: "~=", + [lua.SyntaxKind.LessThanOperator]: "<", + [lua.SyntaxKind.LessEqualOperator]: "<=", + [lua.SyntaxKind.GreaterThanOperator]: ">", + [lua.SyntaxKind.GreaterEqualOperator]: ">=", + [lua.SyntaxKind.AndOperator]: "and", + [lua.SyntaxKind.OrOperator]: "or", + [lua.SyntaxKind.NotOperator]: "not ", + [lua.SyntaxKind.BitwiseAndOperator]: "&", + [lua.SyntaxKind.BitwiseOrOperator]: "|", + [lua.SyntaxKind.BitwiseExclusiveOrOperator]: "~", + [lua.SyntaxKind.BitwiseRightShiftOperator]: ">>", + [lua.SyntaxKind.BitwiseLeftShiftOperator]: "<<", + [lua.SyntaxKind.BitwiseNotOperator]: "~", }; - /* tslint:enable:object-literal-sort-keys */ + private static operatorPrecedence: Record = { + [lua.SyntaxKind.OrOperator]: 1, + [lua.SyntaxKind.AndOperator]: 2, + + [lua.SyntaxKind.EqualityOperator]: 3, + [lua.SyntaxKind.InequalityOperator]: 3, + [lua.SyntaxKind.LessThanOperator]: 3, + [lua.SyntaxKind.LessEqualOperator]: 3, + [lua.SyntaxKind.GreaterThanOperator]: 3, + [lua.SyntaxKind.GreaterEqualOperator]: 3, + + [lua.SyntaxKind.BitwiseOrOperator]: 4, + [lua.SyntaxKind.BitwiseExclusiveOrOperator]: 5, + [lua.SyntaxKind.BitwiseAndOperator]: 6, + + [lua.SyntaxKind.BitwiseLeftShiftOperator]: 7, + [lua.SyntaxKind.BitwiseRightShiftOperator]: 7, + + [lua.SyntaxKind.ConcatOperator]: 8, - private options: CompilerOptions; - private currentIndent: string; + [lua.SyntaxKind.AdditionOperator]: 9, + [lua.SyntaxKind.SubtractionOperator]: 9, - public constructor(options: CompilerOptions) { - this.options = options; - this.currentIndent = ""; + [lua.SyntaxKind.MultiplicationOperator]: 10, + [lua.SyntaxKind.DivisionOperator]: 10, + [lua.SyntaxKind.FloorDivisionOperator]: 10, + [lua.SyntaxKind.ModuloOperator]: 10, + + [lua.SyntaxKind.NotOperator]: 11, + [lua.SyntaxKind.LengthOperator]: 11, + [lua.SyntaxKind.NegationOperator]: 11, + [lua.SyntaxKind.BitwiseNotOperator]: 11, + + [lua.SyntaxKind.PowerOperator]: 12, + }; + private static rightAssociativeOperators = new Set([lua.SyntaxKind.ConcatOperator, lua.SyntaxKind.PowerOperator]); + + protected currentIndent = ""; + protected luaFile: string; + protected relativeSourcePath: string; + protected options: CompilerOptions; + + public static readonly sourceMapTracebackPlaceholder = "{#SourceMapTraceback}"; + + constructor(private emitHost: EmitHost, private program: ts.Program, private sourceFile: string) { + this.options = program.getCompilerOptions(); + this.luaFile = normalizeSlashes(getEmitPath(this.sourceFile, this.program)); + // Source nodes contain relative path from mapped lua file to original TS source file + this.relativeSourcePath = normalizeSlashes(path.relative(path.dirname(this.luaFile), this.sourceFile)); } - public print(block: tstl.Block, luaLibFeatures?: Set): string { - let header = ""; + public print(file: lua.File): PrintResult { + // Add traceback lualib if sourcemap traceback option is enabled + if (this.options.sourceMapTraceback) { + file.luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack); + } + + const sourceRoot = this.options.sourceRoot + ? // According to spec, sourceRoot is simply prepended to the source name, so the slash should be included + `${this.options.sourceRoot.replace(/[\\/]+$/, "")}/` + : ""; + const rootSourceNode = this.printFile(file); + const sourceMap = this.buildSourceMap(sourceRoot, rootSourceNode); - if (this.options.noHeader === undefined || this.options.noHeader === false) { - header += `--[[ Generated with https://github.com/Perryvw/TypescriptToLua ]]\n`; + let code = rootSourceNode.toString(); + + if (this.options.inlineSourceMap) { + code += "\n" + this.printInlineSourceMap(sourceMap); } - if (luaLibFeatures) { - // Require lualib bundle - if ((this.options.luaLibImport === LuaLibImportKind.Require && luaLibFeatures.size > 0) - || this.options.luaLibImport === LuaLibImportKind.Always) - { - header += `require("lualib_bundle");\n`; + if (this.options.sourceMapTraceback) { + const stackTraceOverride = this.printStackTraceOverride(rootSourceNode); + code = code.replace(LuaPrinter.sourceMapTracebackPlaceholder, stackTraceOverride); + } + + return { code, sourceMap: sourceMap.toString(), sourceMapNode: rootSourceNode }; + } + + private printInlineSourceMap(sourceMap: SourceMapGenerator): string { + const map = sourceMap.toString(); + const base64Map = Buffer.from(map).toString("base64"); + + return `--# sourceMappingURL=data:application/json;base64,${base64Map}\n`; + } + + private printStackTraceOverride(rootNode: SourceNode): string { + let currentLine = 1; + const map: Record = {}; + rootNode.walk((chunk, mappedPosition) => { + if (mappedPosition.line !== undefined && mappedPosition.line > 0) { + if (map[currentLine] === undefined) { + map[currentLine] = mappedPosition.line; + } else { + map[currentLine] = Math.min(map[currentLine], mappedPosition.line); + } } + + currentLine += chunk.split("\n").length - 1; + }); + + const mapItems = Object.entries(map).map(([line, original]) => `["${line}"] = ${original}`); + const mapString = "{" + mapItems.join(",") + "}"; + + return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`; + } + + protected printFile(file: lua.File): SourceNode { + let sourceChunks: SourceChunk[] = [file.trivia]; + + if (!this.options.noHeader) { + sourceChunks.push(tstlHeader); + } + + const luaTarget = this.options.luaTarget ?? LuaTarget.Universal; + const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require; + if ( + (luaLibImport === LuaLibImportKind.Require || luaLibImport === LuaLibImportKind.RequireMinimal) && + file.luaLibFeatures.size > 0 + ) { + // Import lualib features + sourceChunks = this.printStatementArray( + loadImportedLualibFeatures(file.luaLibFeatures, luaTarget, this.emitHost) + ); + } else if (luaLibImport === LuaLibImportKind.Inline && file.luaLibFeatures.size > 0) { // Inline lualib features - else if (this.options.luaLibImport === LuaLibImportKind.Inline && luaLibFeatures.size > 0) - { - header += "-- Lua Library inline imports\n"; - header += LuaLib.loadFeatures(luaLibFeatures); - } + sourceChunks.push("-- Lua Library inline imports\n"); + sourceChunks.push(loadInlineLualibFeatures(file.luaLibFeatures, luaTarget, this.emitHost)); + sourceChunks.push("-- End of Lua Library inline imports\n"); } - return header + this.printBlock(block); + if (this.options.sourceMapTraceback && !isBundleEnabled(this.options)) { + // In bundle mode the traceback is being generated for the entire file in getBundleResult + // Otherwise, traceback is being generated locally + sourceChunks.push(`${LuaPrinter.sourceMapTracebackPlaceholder}\n`); + } + + // Print reest of the statements in file + sourceChunks.push(...this.printStatementArray(file.statements)); + + return this.concatNodes(...sourceChunks); } - private pushIndent(): void { - this.currentIndent = this.currentIndent + " "; + protected pushIndent(): void { + this.currentIndent += " "; } - private popIndent(): void { + protected popIndent(): void { this.currentIndent = this.currentIndent.slice(4); } - private indent(input: string): string { - return this.currentIndent + input; + protected indent(input: SourceChunk = ""): SourceChunk { + return this.concatNodes(this.currentIndent, input); } - private printBlock(block: tstl.Block): string { - return this.ignoreDeadStatements(block.statements).map(s => this.printStatement(s)).join(""); + protected createSourceNode(node: lua.Node, chunks: SourceChunk | SourceChunk[], name?: string): SourceNode { + const { line, column } = lua.getOriginalPos(node); + + return line !== undefined && column !== undefined + ? new SourceNode(line + 1, column, this.relativeSourcePath, chunks, name) + : new SourceNode(null, null, this.relativeSourcePath, chunks, name); } - private printStatement(statement: tstl.Statement): string { + protected concatNodes(...chunks: SourceChunk[]): SourceNode { + return new SourceNode(null, null, this.relativeSourcePath, chunks); + } + + protected printBlock(block: lua.Block): SourceNode { + return this.concatNodes(...this.printStatementArray(block.statements)); + } + + private statementMayRequireSemiColon(statement: lua.Statement): boolean { + // Types of statements that could create ambiguous syntax if followed by parenthesis + return ( + lua.isVariableDeclarationStatement(statement) || + lua.isAssignmentStatement(statement) || + lua.isExpressionStatement(statement) + ); + } + + private nodeStartsWithParenthesis(sourceNode: SourceNode): boolean { + let result: boolean | undefined; + sourceNode.walk(chunk => { + if (result === undefined) { + chunk = chunk.trimLeft(); // Ignore leading whitespace + + if (chunk.length > 0) { + result = chunk.startsWith("("); + } + } + }); + return result ?? false; + } + + protected printStatementArray(statements: lua.Statement[]): SourceChunk[] { + const statementNodes: SourceNode[] = []; + for (const [index, statement] of statements.entries()) { + const node = this.printStatement(statement); + + if ( + index > 0 && + this.statementMayRequireSemiColon(statements[index - 1]) && + this.nodeStartsWithParenthesis(node) + ) { + statementNodes[index - 1].add(";"); + } + + statementNodes.push(node); + + if (lua.isReturnStatement(statement)) break; + } + + return statementNodes.length > 0 ? [...intersperse(statementNodes, "\n"), "\n"] : []; + } + + public printStatement(statement: lua.Statement): SourceNode { + let resultNode = this.printStatementExcludingComments(statement); + + if (statement.leadingComments) { + resultNode = this.concatNodes( + statement.leadingComments.map(c => this.printComment(c)).join("\n"), + "\n", + resultNode + ); + } + + if (statement.trailingComments) { + resultNode = this.concatNodes( + resultNode, + "\n", + statement.trailingComments.map(c => this.printComment(c)).join("\n") + ); + } + + return resultNode; + } + + public printComment(comment: string | string[]): SourceChunk { + if (Array.isArray(comment)) { + if (comment.length === 0) { + return this.indent("--[[]]"); + } else { + const [firstLine, ...restLines] = comment; + const commentLines = this.concatNodes(...restLines.map(c => this.concatNodes("\n", this.indent(c)))); + return this.concatNodes(this.indent("--[["), firstLine, commentLines, "]]"); + } + } else { + return this.indent(`--${comment}`); + } + } + + protected printStatementExcludingComments(statement: lua.Statement): SourceNode { switch (statement.kind) { - case tstl.SyntaxKind.DoStatement: - return this.printDoStatement(statement as tstl.DoStatement); - case tstl.SyntaxKind.VariableDeclarationStatement: - return this.printVariableDeclarationStatement(statement as tstl.VariableDeclarationStatement); - case tstl.SyntaxKind.AssignmentStatement: - return this.printVariableAssignmentStatement(statement as tstl.AssignmentStatement); - case tstl.SyntaxKind.IfStatement: - return this.printIfStatement(statement as tstl.IfStatement); - case tstl.SyntaxKind.WhileStatement: - return this.printWhileStatement(statement as tstl.WhileStatement); - case tstl.SyntaxKind.RepeatStatement: - return this.printRepeatStatement(statement as tstl.RepeatStatement); - case tstl.SyntaxKind.ForStatement: - return this.printForStatement(statement as tstl.ForStatement); - case tstl.SyntaxKind.ForInStatement: - return this.printForInStatement(statement as tstl.ForInStatement); - case tstl.SyntaxKind.GotoStatement: - return this.printGotoStatement(statement as tstl.GotoStatement); - case tstl.SyntaxKind.LabelStatement: - return this.printLabelStatement(statement as tstl.LabelStatement); - case tstl.SyntaxKind.ReturnStatement: - return this.printReturnStatement(statement as tstl.ReturnStatement); - case tstl.SyntaxKind.BreakStatement: - return this.printBreakStatement(statement as tstl.BreakStatement); - case tstl.SyntaxKind.ExpressionStatement: - return this.printExpressionStatement(statement as tstl.ExpressionStatement); + case lua.SyntaxKind.DoStatement: + return this.printDoStatement(statement as lua.DoStatement); + case lua.SyntaxKind.VariableDeclarationStatement: + return this.printVariableDeclarationStatement(statement as lua.VariableDeclarationStatement); + case lua.SyntaxKind.AssignmentStatement: + return this.printVariableAssignmentStatement(statement as lua.AssignmentStatement); + case lua.SyntaxKind.IfStatement: + return this.printIfStatement(statement as lua.IfStatement); + case lua.SyntaxKind.WhileStatement: + return this.printWhileStatement(statement as lua.WhileStatement); + case lua.SyntaxKind.RepeatStatement: + return this.printRepeatStatement(statement as lua.RepeatStatement); + case lua.SyntaxKind.ForStatement: + return this.printForStatement(statement as lua.ForStatement); + case lua.SyntaxKind.ForInStatement: + return this.printForInStatement(statement as lua.ForInStatement); + case lua.SyntaxKind.GotoStatement: + return this.printGotoStatement(statement as lua.GotoStatement); + case lua.SyntaxKind.LabelStatement: + return this.printLabelStatement(statement as lua.LabelStatement); + case lua.SyntaxKind.ReturnStatement: + 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: + throw new Error(`Tried to print unknown statement kind: ${lua.SyntaxKind[statement.kind]}`); } } - private printDoStatement(statement: tstl.DoStatement): string { - let result = this.indent("do\n"); + public printDoStatement(statement: lua.DoStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("do\n")); this.pushIndent(); - result += this.ignoreDeadStatements(statement.statements).map(s => this.printStatement(s)).join(""); + chunks.push(...this.printStatementArray(statement.statements)); this.popIndent(); - result += this.indent("end\n"); + chunks.push(this.indent("end")); - return result; + return this.concatNodes(...chunks); } - private printVariableDeclarationStatement(statement: tstl.VariableDeclarationStatement): string { - const left = this.indent(`local ${statement.left.map(e => this.printExpression(e)).join(", ")}`); - if (statement.right) { - return left + ` = ${statement.right.map(e => this.printExpression(e)).join(", ")};\n`; + public printVariableDeclarationStatement(statement: lua.VariableDeclarationStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("local ")); + + if (lua.isFunctionDefinition(statement)) { + // Print all local functions as `local function foo()` instead of `local foo = function` to allow recursion + chunks.push(this.printFunctionDefinition(statement)); } else { - return left + ";\n"; + chunks.push(...this.joinChunksWithComma(statement.left.map(e => this.printExpression(e)))); + + if (statement.right) { + chunks.push(" = "); + chunks.push(...this.joinChunksWithComma(statement.right.map(e => this.printExpression(e)))); + } } + + return this.createSourceNode(statement, chunks); } - private printVariableAssignmentStatement(statement: tstl.AssignmentStatement): string { - return this.indent( - `${statement.left.map(e => this.printExpression(e)).join(", ")} = ` + - `${statement.right.map(e => this.printExpression(e)).join(", ")};\n`); + public printVariableAssignmentStatement(statement: lua.AssignmentStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent()); + + if (lua.isFunctionDefinition(statement) && (statement.right[0].flags & lua.NodeFlags.Declaration) !== 0) { + // Use `function foo()` instead of `foo = function()` + const name = this.printExpression(statement.left[0]); + if (isValidLuaFunctionDeclarationName(name.toString(), this.options)) { + chunks.push(this.printFunctionDefinition(statement)); + return this.createSourceNode(statement, chunks); + } + } + + chunks.push(...this.joinChunksWithComma(statement.left.map(e => this.printExpression(e)))); + chunks.push(" = "); + chunks.push(...this.joinChunksWithComma(statement.right.map(e => this.printExpression(e)))); + + return this.createSourceNode(statement, chunks); } - private printIfStatement(statement: tstl.IfStatement, isElseIf?: boolean): string { + public printIfStatement(statement: lua.IfStatement, isElseIf = false): SourceNode { + const chunks: SourceChunk[] = []; + const prefix = isElseIf ? "elseif" : "if"; - let result = this.indent(`${prefix} ${this.printExpression(statement.condtion)} then\n`); + chunks.push(this.indent(prefix + " "), this.printExpression(statement.condition), " then\n"); + this.pushIndent(); - result += this.printBlock(statement.ifBlock); + chunks.push(this.printBlock(statement.ifBlock)); this.popIndent(); + if (statement.elseBlock) { - if (tstl.isIfStatement(statement.elseBlock)) { - result += this.printIfStatement(statement.elseBlock, true); + if (lua.isIfStatement(statement.elseBlock)) { + chunks.push(this.printIfStatement(statement.elseBlock, true)); } else { - result += this.indent("else\n"); + chunks.push(this.indent("else\n")); this.pushIndent(); - result += this.printBlock(statement.elseBlock); + chunks.push(this.printBlock(statement.elseBlock)); this.popIndent(); - result += this.indent("end\n"); + chunks.push(this.indent("end")); } } else { - result += this.indent("end\n"); + chunks.push(this.indent("end")); } - return result; + return this.concatNodes(...chunks); } - private printWhileStatement(statement: tstl.WhileStatement): string { - let result = this.indent(`while ${this.printExpression(statement.condtion)} do\n`); + public printWhileStatement(statement: lua.WhileStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("while "), this.printExpression(statement.condition), " do\n"); + this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent("end\n"); - return result; + chunks.push(this.indent("end")); + + return this.concatNodes(...chunks); } - private printRepeatStatement(statement: tstl.RepeatStatement): string { - let result = this.indent(`repeat\n`); + public printRepeatStatement(statement: lua.RepeatStatement): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("repeat\n")); + this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent(`until ${this.printExpression(statement.condtion)};\n`); - return result; + chunks.push(this.indent("until "), this.printExpression(statement.condition)); + + return this.concatNodes(...chunks); } - private printForStatement(statement: tstl.ForStatement): string { + public printForStatement(statement: lua.ForStatement): SourceNode { const ctrlVar = this.printExpression(statement.controlVariable); const ctrlVarInit = this.printExpression(statement.controlVariableInitializer); const limit = this.printExpression(statement.limitExpression); - let result = this.indent(`for ${ctrlVar} = ${ctrlVarInit}, ${limit}`); + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("for "), ctrlVar, " = ", ctrlVarInit, ", ", limit); + if (statement.stepExpression) { - const step = this.printExpression(statement.stepExpression); - result += `, ${step}`; + chunks.push(", ", this.printExpression(statement.stepExpression)); } - result += ` do\n`; + chunks.push(" do\n"); this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent("end\n"); - return result; + chunks.push(this.indent("end")); + + return this.concatNodes(...chunks); } - private printForInStatement(statement: tstl.ForInStatement): string { - const names = statement.names.map(i => this.printIdentifier(i)).join(", "); - const expressions = statement.expressions.map(e => this.printExpression(e)).join(", "); + public printForInStatement(statement: lua.ForInStatement): SourceNode { + const names = this.joinChunksWithComma(statement.names.map(i => this.printIdentifier(i))); + const expressions = this.joinChunksWithComma(statement.expressions.map(e => this.printExpression(e))); + + const chunks: SourceChunk[] = []; + + chunks.push(this.indent("for "), ...names, " in ", ...expressions, " do\n"); - let result = this.indent(`for ${names} in ${expressions} do\n`); this.pushIndent(); - result += this.printBlock(statement.body); + chunks.push(this.printBlock(statement.body)); this.popIndent(); - result += this.indent("end\n"); + chunks.push(this.indent("end")); - return result; + return this.createSourceNode(statement, chunks); } - private printGotoStatement(statement: tstl.GotoStatement): string { - return this.indent(`goto ${statement.label};\n`); + public printGotoStatement(statement: lua.GotoStatement): SourceNode { + return this.createSourceNode(statement, [this.indent("goto "), statement.label]); } - private printLabelStatement(statement: tstl.LabelStatement): string { - return this.indent(`::${statement.name}::\n`); + public printLabelStatement(statement: lua.LabelStatement): SourceNode { + return this.createSourceNode(statement, [this.indent("::"), statement.name, "::"]); } - private printReturnStatement(statement: tstl.ReturnStatement): string { - if (!statement.expressions) { - return this.indent(`return;\n`); + public printReturnStatement(statement: lua.ReturnStatement): SourceNode { + if (statement.expressions.length === 0) { + return this.createSourceNode(statement, this.indent("return")); } - return this.indent(`return ${statement.expressions.map(e => this.printExpression(e)).join(", ")};\n`); + + const chunks: SourceChunk[] = []; + + chunks.push(...this.joinChunksWithComma(statement.expressions.map(e => this.printExpression(e)))); + + return this.createSourceNode(statement, [this.indent(), "return ", ...chunks]); + } + + public printBreakStatement(statement: lua.BreakStatement): SourceNode { + return this.createSourceNode(statement, this.indent("break")); } - private printBreakStatement(statement: tstl.BreakStatement): string { - return this.indent("break;\n"); + public printContinueStatement(statement: lua.ContinueStatement): SourceNode { + return this.createSourceNode(statement, this.indent("continue")); } - private printExpressionStatement(statement: tstl.ExpressionStatement): string { - return this.indent(`${this.printExpression(statement.expression)};\n`); + public printExpressionStatement(statement: lua.ExpressionStatement): SourceNode { + return this.createSourceNode(statement, [this.indent(), this.printExpression(statement.expression)]); } // Expressions - private printExpression(expression: tstl.Expression): string { + public printExpression(expression: lua.Expression): SourceNode { switch (expression.kind) { - case tstl.SyntaxKind.StringLiteral: - return this.printStringLiteral(expression as tstl.StringLiteral); - case tstl.SyntaxKind.NumericLiteral: - return this.printNumericLiteral(expression as tstl.NumericLiteral); - case tstl.SyntaxKind.NilKeyword: - return this.printNilLiteral(expression as tstl.NilLiteral); - case tstl.SyntaxKind.DotsKeyword: - return this.printDotsLiteral(expression as tstl.DotsLiteral); - case tstl.SyntaxKind.TrueKeyword: - case tstl.SyntaxKind.FalseKeyword: - return this.printBooleanLiteral(expression as tstl.BooleanLiteral); - case tstl.SyntaxKind.FunctionExpression: - return this.printFunctionExpression(expression as tstl.FunctionExpression); - case tstl.SyntaxKind.TableFieldExpression: - return this.printTableFieldExpression(expression as tstl.TableFieldExpression); - case tstl.SyntaxKind.TableExpression: - return this.printTableExpression(expression as tstl.TableExpression); - case tstl.SyntaxKind.UnaryExpression: - return this.printUnaryExpression(expression as tstl.UnaryExpression); - case tstl.SyntaxKind.BinaryExpression: - return this.printBinaryExpression(expression as tstl.BinaryExpression); - case tstl.SyntaxKind.ParenthesizedExpression: - return this.printParenthesizedExpression(expression as tstl.ParenthesizedExpression); - case tstl.SyntaxKind.CallExpression: - return this.printCallExpression(expression as tstl.CallExpression); - case tstl.SyntaxKind.MethodCallExpression: - return this.printMethodCallExpression(expression as tstl.MethodCallExpression); - case tstl.SyntaxKind.Identifier: - return this.printIdentifier(expression as tstl.Identifier); - case tstl.SyntaxKind.TableIndexExpression: - return this.printTableIndexExpression(expression as tstl.TableIndexExpression); + case lua.SyntaxKind.StringLiteral: + return this.printStringLiteral(expression as lua.StringLiteral); + case lua.SyntaxKind.NumericLiteral: + return this.printNumericLiteral(expression as lua.NumericLiteral); + case lua.SyntaxKind.NilKeyword: + return this.printNilLiteral(expression as lua.NilLiteral); + case lua.SyntaxKind.DotsKeyword: + return this.printDotsLiteral(expression as lua.DotsLiteral); + case lua.SyntaxKind.ArgKeyword: + return this.printArgLiteral(expression as lua.ArgLiteral); + case lua.SyntaxKind.TrueKeyword: + case lua.SyntaxKind.FalseKeyword: + return this.printBooleanLiteral(expression as lua.BooleanLiteral); + case lua.SyntaxKind.FunctionExpression: + return this.printFunctionExpression(expression as lua.FunctionExpression); + case lua.SyntaxKind.TableFieldExpression: + return this.printTableFieldExpression(expression as lua.TableFieldExpression); + case lua.SyntaxKind.TableExpression: + return this.printTableExpression(expression as lua.TableExpression); + case lua.SyntaxKind.UnaryExpression: + return this.printUnaryExpression(expression as lua.UnaryExpression); + case lua.SyntaxKind.BinaryExpression: + return this.printBinaryExpression(expression as lua.BinaryExpression); + case lua.SyntaxKind.CallExpression: + return this.printCallExpression(expression as lua.CallExpression); + case lua.SyntaxKind.MethodCallExpression: + return this.printMethodCallExpression(expression as lua.MethodCallExpression); + case lua.SyntaxKind.Identifier: + return this.printIdentifier(expression as lua.Identifier); + case lua.SyntaxKind.TableIndexExpression: + 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]}`); } } - private printStringLiteral(expression: tstl.StringLiteral): string { - return `"${expression.value}"`; + public printStringLiteral(expression: lua.StringLiteral): SourceNode { + return this.createSourceNode(expression, escapeString(expression.value)); } - private printNumericLiteral(expression: tstl.NumericLiteral): string { - return `${expression.value}`; + public printNumericLiteral(expression: lua.NumericLiteral): SourceNode { + return this.createSourceNode(expression, String(expression.value)); } - private printNilLiteral(expression: tstl.NilLiteral): string { - return "nil"; + public printNilLiteral(expression: lua.NilLiteral): SourceNode { + return this.createSourceNode(expression, "nil"); } - private printDotsLiteral(expression: tstl.DotsLiteral): string { - return "..."; + public printDotsLiteral(expression: lua.DotsLiteral): SourceNode { + return this.createSourceNode(expression, "..."); } - private printBooleanLiteral(expression: tstl.BooleanLiteral): string { - if (expression.kind === tstl.SyntaxKind.TrueKeyword) { - return "true"; - } else { - return "false"; - } + public printArgLiteral(expression: lua.ArgLiteral): SourceNode { + return this.createSourceNode(expression, "arg"); } - private printFunctionExpression(expression: tstl.FunctionExpression): string { - const paramterArr: string[] = expression.params ? expression.params.map(i => this.printIdentifier(i)) : []; + public printBooleanLiteral(expression: lua.BooleanLiteral): SourceNode { + return this.createSourceNode(expression, expression.kind === lua.SyntaxKind.TrueKeyword ? "true" : "false"); + } + + private printFunctionParameters(expression: lua.FunctionExpression): SourceChunk[] { + const parameterChunks = (expression.params ?? []).map(i => this.printIdentifier(i)); + if (expression.dots) { - paramterArr.push(this.printDotsLiteral(expression.dots)); + parameterChunks.push(this.printDotsLiteral(expression.dots)); + } + + return this.joinChunksWithComma(parameterChunks); + } + + public printFunctionExpression(expression: lua.FunctionExpression): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push("function("); + chunks.push(...this.printFunctionParameters(expression)); + chunks.push(")"); + + if (lua.isInlineFunctionExpression(expression)) { + const returnStatement = expression.body.statements[0]; + chunks.push(" "); + const returnNode: SourceChunk[] = [ + "return ", + ...this.joinChunksWithComma(returnStatement.expressions.map(e => this.printExpression(e))), + ]; + chunks.push(this.createSourceNode(returnStatement, returnNode)); + chunks.push(this.createSourceNode(expression, " end")); + } else { + chunks.push("\n"); + this.pushIndent(); + chunks.push(this.printBlock(expression.body)); + this.popIndent(); + chunks.push(this.indent(this.createSourceNode(expression, "end"))); } - let result = `function(${paramterArr.join(", ")})\n`; + return this.createSourceNode(expression, chunks); + } + + public printFunctionDefinition(statement: lua.FunctionDefinition): SourceNode { + const expression = statement.right[0]; + const chunks: SourceChunk[] = []; + + chunks.push("function "); + chunks.push(this.printExpression(statement.left[0])); + chunks.push("("); + chunks.push(...this.printFunctionParameters(expression)); + chunks.push(")\n"); + this.pushIndent(); - result += this.printBlock(expression.body); + chunks.push(this.printBlock(expression.body)); this.popIndent(); - result += this.indent("end"); + chunks.push(this.indent(this.createSourceNode(statement, "end"))); - return result; + return this.createSourceNode(expression, chunks); } - private printTableFieldExpression(expression: tstl.TableFieldExpression): string { + public printTableFieldExpression(expression: lua.TableFieldExpression): SourceNode { + const chunks: SourceChunk[] = []; + const value = this.printExpression(expression.value); if (expression.key) { - if (tstl.isStringLiteral(expression.key) && tsHelper.isValidLuaIdentifier(expression.key.value)) { - return `${expression.key.value} = ${value}`; + if (lua.isStringLiteral(expression.key) && isValidLuaIdentifier(expression.key.value, this.options)) { + chunks.push(expression.key.value, " = ", value); } else { - return `[${this.printExpression(expression.key)}] = ${value}`; + chunks.push("[", this.printExpression(expression.key), "] = ", value); } } else { - return value; + chunks.push(value); } - } - private printTableExpression(expression: tstl.TableExpression): string { - let fields = ""; - if (expression.fields) { - fields = expression.fields.map(f => this.printTableFieldExpression(f)).join(", "); - } - return `{${fields}}`; + return this.createSourceNode(expression, chunks); } - private printUnaryExpression(expression: tstl.UnaryExpression): string { - const operand = this.needsParentheses(expression.operand) - ? `(${this.printExpression(expression.operand)})` - : this.printExpression(expression.operand); - return `${this.printOperator(expression.operator)}${operand}`; + public printTableExpression(expression: lua.TableExpression): SourceNode { + return this.createSourceNode(expression, ["{", ...this.printExpressionList(expression.fields), "}"]); } - private printBinaryExpression(expression: tstl.BinaryExpression): string { - const left = this.needsParentheses(expression.left) - ? `(${this.printExpression(expression.left)})` - : this.printExpression(expression.left); + public printUnaryExpression(expression: lua.UnaryExpression): SourceNode { + const chunks: SourceChunk[] = []; - const right = this.needsParentheses(expression.right) - ? `(${this.printExpression(expression.right)})` - : this.printExpression(expression.right); + chunks.push(this.printOperator(expression.operator)); + chunks.push( + this.printExpressionInParenthesesIfNeeded( + expression.operand, + LuaPrinter.operatorPrecedence[expression.operator] + ) + ); - const operator = this.printOperator(expression.operator); - return `${left} ${operator} ${right}`; + return this.createSourceNode(expression, chunks); } - private needsParentheses(expression: tstl.Expression): boolean { - return tstl.isBinaryExpression(expression) || tstl.isUnaryExpression(expression) - || tstl.isFunctionExpression(expression); + public printBinaryExpression(expression: lua.BinaryExpression): SourceNode { + const chunks: SourceChunk[] = []; + const isRightAssociative = LuaPrinter.rightAssociativeOperators.has(expression.operator); + const precedence = LuaPrinter.operatorPrecedence[expression.operator]; + chunks.push( + this.printExpressionInParenthesesIfNeeded(expression.left, isRightAssociative ? precedence + 1 : precedence) + ); + chunks.push(" ", this.printOperator(expression.operator), " "); + chunks.push( + this.printExpressionInParenthesesIfNeeded( + expression.right, + isRightAssociative ? precedence : precedence + 1 + ) + ); + + return this.createSourceNode(expression, chunks); } - private printParenthesizedExpression(expression: tstl.ParenthesizedExpression): string { - return `(${this.printExpression(expression.innerEpxression)})`; + private printExpressionInParenthesesIfNeeded(expression: lua.Expression, minPrecedenceToOmit?: number): SourceNode { + return this.needsParenthesis(expression, minPrecedenceToOmit) + ? this.createSourceNode(expression, ["(", this.printExpression(expression), ")"]) + : this.printExpression(expression); } - private printCallExpression(expression: tstl.CallExpression): string { - const params = expression.params ? expression.params.map(e => this.printExpression(e)).join(", ") : ""; - return this.needsParentheses(expression.expression) - ? `(${this.printExpression(expression.expression)})(${params})` - : `${this.printExpression(expression.expression)}(${params})`; + private needsParenthesis(expression: lua.Expression, minPrecedenceToOmit?: number): boolean { + if (lua.isBinaryExpression(expression) || lua.isUnaryExpression(expression)) { + return ( + minPrecedenceToOmit === undefined || + LuaPrinter.operatorPrecedence[expression.operator] < minPrecedenceToOmit + ); + } else { + return lua.isFunctionExpression(expression) || lua.isTableExpression(expression); + } + } + + public printCallExpression(expression: lua.CallExpression): SourceNode { + const chunks = []; + + chunks.push(this.printExpressionInParenthesesIfNeeded(expression.expression), "("); + + if (expression.params) { + chunks.push(...this.printExpressionList(expression.params)); + } + + chunks.push(")"); + + return this.createSourceNode(expression, chunks); } - private printMethodCallExpression(expression: tstl.MethodCallExpression): string { - const params = expression.params.map(e => this.printExpression(e)).join(", "); - const prefix = this.printExpression(expression.prefixExpression); + public printMethodCallExpression(expression: lua.MethodCallExpression): SourceNode { + const chunks = []; + + const prefix = + this.needsParenthesis(expression.prefixExpression) || lua.isStringLiteral(expression.prefixExpression) + ? ["(", this.printExpression(expression.prefixExpression), ")"] + : [this.printExpression(expression.prefixExpression)]; + const name = this.printIdentifier(expression.name); - return `${prefix}:${name}(${params})`; + + chunks.push(...prefix, ":", name, "("); + + if (expression.params) { + chunks.push(...this.printExpressionList(expression.params)); + } + + chunks.push(")"); + + return this.createSourceNode(expression, chunks); } - private printIdentifier(expression: tstl.Identifier): string { - return expression.text; + public printIdentifier(expression: lua.Identifier): SourceNode { + return this.createSourceNode( + expression, + expression.text, + expression.originalName !== expression.text ? expression.originalName : undefined + ); } - private printTableIndexExpression(expression: tstl.TableIndexExpression): string { - const table = this.printExpression(expression.table); - if (tstl.isStringLiteral(expression.index) && tsHelper.isValidLuaIdentifier(expression.index.value)) { - return `${table}.${expression.index.value}`; + public printTableIndexExpression(expression: lua.TableIndexExpression): SourceNode { + const chunks: SourceChunk[] = []; + + chunks.push(this.printExpressionInParenthesesIfNeeded(expression.table)); + if (lua.isStringLiteral(expression.index) && isValidLuaIdentifier(expression.index.value, this.options)) { + chunks.push(".", this.createSourceNode(expression.index, expression.index.value)); + } else { + chunks.push("[", this.printExpression(expression.index), "]"); } - return `${table}[${this.printExpression(expression.index)}]`; + return this.createSourceNode(expression, chunks); + } + + public printParenthesizedExpression(expression: lua.ParenthesizedExpression) { + return this.createSourceNode(expression, ["(", this.printExpression(expression.expression), ")"]); } - private printOperator(kind: tstl.Operator): string { - return LuaPrinter.operatorMap[kind]; + 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), + ]); } - private ignoreDeadStatements(statements: tstl.Statement[]): tstl.Statement[] { - const aliveStatements = []; - for (const statement of statements) { - aliveStatements.push(statement); - if (tstl.isReturnStatement(statement)) { - break; + public printOperator(kind: lua.Operator): SourceNode { + return new SourceNode(null, null, this.relativeSourcePath, LuaPrinter.operatorMap[kind]); + } + + protected joinChunksWithComma(chunks: SourceChunk[]): SourceChunk[] { + return intersperse(chunks, ", "); + } + + /** + * Returns true if the expression list (table field or parameters) should be printed on one line. + */ + protected isSimpleExpressionList(expressions: lua.Expression[]): boolean { + if (expressions.length <= 1) return true; + if (expressions.length > 4) return false; + return expressions.every(isSimpleExpression); + } + + protected printExpressionList(expressions: lua.Expression[]): SourceChunk[] { + const chunks: SourceChunk[] = []; + + if (this.isSimpleExpressionList(expressions)) { + chunks.push(...this.joinChunksWithComma(expressions.map(e => this.printExpression(e)))); + } else { + chunks.push("\n"); + this.pushIndent(); + for (const [index, expression] of expressions.entries()) { + const tail = index < expressions.length - 1 ? ",\n" : "\n"; + chunks.push(this.indent(), this.printExpression(expression), tail); } + this.popIndent(); + chunks.push(this.indent()); } - return aliveStatements; + + return chunks; + } + + // The key difference between this and SourceNode.toStringWithSourceMap() is that SourceNodes with null line/column + // will not generate 'empty' mappings in the source map that point to nothing in the original TS. + private buildSourceMap(sourceRoot: string, rootSourceNode: SourceNode): SourceMapGenerator { + const map = new SourceMapGenerator({ + file: path.basename(this.luaFile), + sourceRoot, + }); + + let generatedLine = 1; + let generatedColumn = 0; + let currentMapping: Mapping | undefined; + + const isNewMapping = (sourceNode: SourceNode) => { + if (sourceNode.line === null) { + return false; + } + if (currentMapping === undefined) { + return true; + } + if ( + currentMapping.generated.line === generatedLine && + currentMapping.generated.column === generatedColumn && + currentMapping.name === sourceNode.name + ) { + return false; + } + return ( + currentMapping.original.line !== sourceNode.line || + currentMapping.original.column !== sourceNode.column || + currentMapping.name !== sourceNode.name + ); + }; + + const build = (sourceNode: SourceNode) => { + if (isNewMapping(sourceNode)) { + currentMapping = { + source: sourceNode.source, + original: { line: sourceNode.line, column: sourceNode.column }, + generated: { line: generatedLine, column: generatedColumn }, + name: sourceNode.name, + }; + map.addMapping(currentMapping); + } + + for (const chunk of sourceNode.children as SourceChunk[]) { + if (typeof chunk === "string") { + const lines = chunk.split("\n"); + if (lines.length > 1) { + generatedLine += lines.length - 1; + generatedColumn = 0; + currentMapping = undefined; // Mappings end at newlines + } + generatedColumn += lines[lines.length - 1].length; + } else { + build(chunk); + } + } + }; + build(rootSourceNode); + + return map; } } diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts deleted file mode 100644 index c72debaeb..000000000 --- a/src/LuaTransformer.ts +++ /dev/null @@ -1,4350 +0,0 @@ -import * as path from "path"; -import * as ts from "typescript"; - -import {CompilerOptions, LuaLibImportKind, LuaTarget} from "./CompilerOptions"; -import {DecoratorKind} from "./Decorator"; -import * as tstl from "./LuaAST"; -import {LuaLib, LuaLibFeature} from "./LuaLib"; -import {ContextType, TSHelper as tsHelper} from "./TSHelper"; -import {TSTLErrors} from "./TSTLErrors"; - -export type StatementVisitResult = tstl.Statement | tstl.Statement[] | undefined; -export type ExpressionVisitResult = tstl.Expression | undefined; -export enum ScopeType { - File = 0x1, - Function = 0x2, - Switch = 0x4, - Loop = 0x8, - Conditional = 0x10, - Block = 0x20, -} - -interface SymbolInfo { - symbol: ts.Symbol; - firstSeenAtPos: number; -} - -interface FunctionDefinitionInfo { - referencedSymbols: Set; - assignment?: tstl.AssignmentStatement; -} - -interface Scope { - type: ScopeType; - id: number; - referencedSymbols?: Set; - variableDeclarations?: tstl.VariableDeclarationStatement[]; - functionDefinitions?: Map; -} - -export class LuaTransformer { - public luaKeywords: Set = new Set([ - "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "new", "nil", - "not", "or", "repeat", "return", "self", "then", "until", "while", - ]); - - private isStrict = true; - - private checker: ts.TypeChecker; - protected options: CompilerOptions; - private isModule: boolean; - - private currentSourceFile?: ts.SourceFile; - - private currentNamespace: ts.ModuleDeclaration; - private classStack: ts.ClassLikeDeclaration[]; - - private scopeStack: Scope[]; - private genVarCounter: number; - - private luaLibFeatureSet: Set; - - private symbolInfo: Map; - private symbolIds: Map; - private genSymbolIdCounter: number; - - private readonly typeValidationCache: Map> = new Map>(); - - public constructor(program: ts.Program, options: CompilerOptions) { - this.checker = program.getTypeChecker(); - this.options = options; - this.isStrict = this.options.alwaysStrict || (this.options.strict && this.options.alwaysStrict !== false) || - (this.isModule && this.options.target && this.options.target >= ts.ScriptTarget.ES2015); - - if (!this.options.luaTarget) { - this.options.luaTarget = LuaTarget.LuaJIT; - } - - this.setupState(); - } - - public setupState(): void { - this.genVarCounter = 0; - this.currentSourceFile = undefined; - this.isModule = false; - this.scopeStack = []; - this.classStack = []; - this.luaLibFeatureSet = new Set(); - this.symbolIds = new Map(); - this.symbolInfo = new Map(); - this.genSymbolIdCounter = 1; - } - - // TODO make all other methods private??? - public transformSourceFile(node: ts.SourceFile): [tstl.Block, Set] { - this.setupState(); - - this.currentSourceFile = node; - - let statements: tstl.Statement[] = []; - if (node.flags & ts.NodeFlags.JsonFile) { - this.isModule = false; - - const statement = node.statements[0]; - if (!statement || !ts.isExpressionStatement(statement)) { - throw TSTLErrors.InvalidJsonFileContent(node); - } - - statements.push(tstl.createReturnStatement([this.transformExpression(statement.expression)])); - } else { - this.pushScope(ScopeType.File, node); - - this.isModule = tsHelper.isFileModule(node); - statements = this.performHoisting(this.transformStatements(node.statements)); - - this.popScope(); - - if (this.isModule) { - statements.unshift( - tstl.createVariableDeclarationStatement( - tstl.createIdentifier("exports"), - tstl.createBinaryExpression( - tstl.createIdentifier("exports"), - tstl.createTableExpression(), - tstl.SyntaxKind.OrOperator - ))); - statements.push( - tstl.createReturnStatement( - [tstl.createIdentifier("exports")] - )); - } - } - - return [tstl.createBlock(statements, node), this.luaLibFeatureSet]; - } - - public transformStatement(node: ts.Statement): StatementVisitResult { - // Ignore declarations - if (node.modifiers && node.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword)) { - return undefined; - } - - switch (node.kind) { - // Block - case ts.SyntaxKind.Block: - return this.transformBlockAsDoStatement(node as ts.Block); - // Declaration Statements - case ts.SyntaxKind.ExportDeclaration: - return this.transformExportDeclaration(node as ts.ExportDeclaration); - case ts.SyntaxKind.ImportDeclaration: - return this.transformImportDeclaration(node as ts.ImportDeclaration); - case ts.SyntaxKind.ClassDeclaration: - return this.transformClassDeclaration(node as ts.ClassDeclaration); - case ts.SyntaxKind.ModuleDeclaration: - return this.transformModuleDeclaration(node as ts.ModuleDeclaration); - case ts.SyntaxKind.EnumDeclaration: - return this.transformEnumDeclaration(node as ts.EnumDeclaration); - case ts.SyntaxKind.FunctionDeclaration: - return this.transformFunctionDeclaration(node as ts.FunctionDeclaration); - case ts.SyntaxKind.TypeAliasDeclaration: - return this.transformTypeAliasDeclaration(node as ts.TypeAliasDeclaration); - case ts.SyntaxKind.InterfaceDeclaration: - return this.transformInterfaceDeclaration(node as ts.InterfaceDeclaration); - // Statements - case ts.SyntaxKind.VariableStatement: - return this.transformVariableStatement(node as ts.VariableStatement); - case ts.SyntaxKind.ExpressionStatement: - return this.transformExpressionStatement(node as ts.ExpressionStatement); - case ts.SyntaxKind.ReturnStatement: - return this.transformReturn(node as ts.ReturnStatement); - case ts.SyntaxKind.IfStatement: - return this.transformIfStatement(node as ts.IfStatement); - case ts.SyntaxKind.WhileStatement: - return this.transformWhileStatement(node as ts.WhileStatement); - case ts.SyntaxKind.DoStatement: - return this.transformDoStatement(node as ts.DoStatement); - case ts.SyntaxKind.ForStatement: - return this.transformForStatement(node as ts.ForStatement); - case ts.SyntaxKind.ForOfStatement: - return this.transformForOfStatement(node as ts.ForOfStatement); - case ts.SyntaxKind.ForInStatement: - return this.transformForInStatement(node as ts.ForInStatement); - case ts.SyntaxKind.SwitchStatement: - return this.transformSwitchStatement(node as ts.SwitchStatement); - case ts.SyntaxKind.BreakStatement: - return this.transformBreakStatement(node as ts.BreakStatement); - case ts.SyntaxKind.TryStatement: - return this.transformTryStatement(node as ts.TryStatement); - case ts.SyntaxKind.ThrowStatement: - return this.transformThrowStatement(node as ts.ThrowStatement); - case ts.SyntaxKind.ContinueStatement: - return this.transformContinueStatement(node as ts.ContinueStatement); - case ts.SyntaxKind.EmptyStatement: - return this.transformEmptyStatement(node as ts.EmptyStatement); - case ts.SyntaxKind.NotEmittedStatement: - return undefined; - default: - throw TSTLErrors.UnsupportedKind("Statement", node.kind, node); - } - } - - /** Converts an array of ts.Statements into an array of tstl.Statements */ - public transformStatements(statements: ts.Statement[] | ReadonlyArray): tstl.Statement[] { - const tstlStatements: tstl.Statement[] = []; - (statements as ts.Statement[]).forEach(statement => { - tstlStatements.push(...this.statementVisitResultToStatementArray(this.transformStatement(statement))); - }); - return tstlStatements; - } - - public transformBlock(block: ts.Block): tstl.Block { - this.pushScope(ScopeType.Block, block); - const statements = this.performHoisting(this.transformStatements(block.statements)); - this.popScope(); - return tstl.createBlock(statements, block); - } - - public transformBlockAsDoStatement(block: ts.Block): tstl.DoStatement { - this.pushScope(ScopeType.Block, block); - const statements = this.performHoisting(this.transformStatements(block.statements)); - this.popScope(); - return tstl.createDoStatement(statements, block); - } - - public transformExportDeclaration(statement: ts.ExportDeclaration): StatementVisitResult { - if (statement.moduleSpecifier === undefined) { - const result = []; - for (const exportElement of statement.exportClause.elements) { - result.push( - tstl.createAssignmentStatement( - this.createExportedIdentifier(this.transformIdentifier(exportElement.name)), - this.transformIdentifier(exportElement.propertyName || exportElement.name) - ) - ); - } - return result; - } - - if (statement.exportClause) { - if (statement.exportClause.elements.some(e => - (e.name && e.name.originalKeywordKind === ts.SyntaxKind.DefaultKeyword) - || (e.propertyName && e.propertyName.originalKeywordKind === ts.SyntaxKind.DefaultKeyword)) - ) { - throw TSTLErrors.UnsupportedDefaultExport(statement); - } - - // First transpile as import clause - const importClause = ts.createImportClause( - undefined, - ts.createNamedImports(statement.exportClause.elements - .map(e => ts.createImportSpecifier(e.propertyName, e.name)) - ) - ); - - const importDeclaration = ts.createImportDeclaration( - statement.decorators, - statement.modifiers, - importClause, - statement.moduleSpecifier - ); - - const importResult = this.transformImportDeclaration(importDeclaration); - - const result = Array.isArray(importResult) ? importResult : [importResult]; - - // Now the module is imported, add the imports to the export table - for (const exportVariable of statement.exportClause.elements) { - result.push( - tstl.createAssignmentStatement( - this.createExportedIdentifier(this.transformIdentifier(exportVariable.name)), - this.transformIdentifier(exportVariable.name) - ) - ); - } - - // Wrap this in a DoStatement to prevent polluting the scope. - return tstl.createDoStatement(result, statement); - } else { - const moduleRequire = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral); - const tempModuleIdentifier = tstl.createIdentifier("__TSTL_export"); - - const declaration = tstl.createVariableDeclarationStatement(tempModuleIdentifier, moduleRequire); - - const forKey = tstl.createIdentifier("____exportKey"); - const forValue = tstl.createIdentifier("____exportValue"); - - const body = tstl.createBlock( - [tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - tstl.createIdentifier("exports"), - forKey - ), - forValue - )] - ); - - const pairsIdentifier = tstl.createIdentifier("pairs"); - const forIn = tstl.createForInStatement( - body, - [tstl.cloneIdentifier(forKey), tstl.cloneIdentifier(forValue)], - [tstl.createCallExpression(pairsIdentifier, [tstl.cloneIdentifier(tempModuleIdentifier)])] - ); - - // Wrap this in a DoStatement to prevent polluting the scope. - return tstl.createDoStatement([declaration, forIn], statement); - } - } - - public transformImportDeclaration(statement: ts.ImportDeclaration): StatementVisitResult { - if (statement.importClause && !statement.importClause.namedBindings) { - throw TSTLErrors.DefaultImportsNotSupported(statement); - } - - const result: tstl.Statement[] = []; - - const moduleSpecifier = statement.moduleSpecifier as ts.StringLiteral; - const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), ""); - - const requireCall = this.createModuleRequire(statement.moduleSpecifier as ts.StringLiteral); - - if (!statement.importClause) { - result.push(tstl.createExpressionStatement(requireCall)); - return result; - } - - const imports = statement.importClause.namedBindings; - if (ts.isNamedImports(imports)) { - const filteredElements = imports.elements.filter(e => { - const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(e), this.checker); - return !decorators.has(DecoratorKind.Extension) && !decorators.has(DecoratorKind.MetaExtension); - }); - - // Elide import if all imported types are extension classes - if (filteredElements.length === 0) { - return undefined; - } - - const tstlIdentifier = (name: string) => "__TSTL_" + name.replace(new RegExp("-|\\$| |#|'", "g"), "_"); - const importUniqueName = tstl.createIdentifier(tstlIdentifier(path.basename((importPath)))); - const requireStatement = tstl.createVariableDeclarationStatement( - tstl.createIdentifier(tstlIdentifier(path.basename((importPath)))), - requireCall, - statement - ); - result.push(requireStatement); - - filteredElements.forEach(importSpecifier => { - if (importSpecifier.propertyName) { - const propertyIdentifier = this.transformIdentifier(importSpecifier.propertyName); - const propertyName = tstl.createStringLiteral(propertyIdentifier.text); - const renamedImport = this.createHoistableVariableDeclarationStatement( - importSpecifier.name, - tstl.createTableIndexExpression(importUniqueName, propertyName), - importSpecifier); - result.push(renamedImport); - } else { - const name = tstl.createStringLiteral(importSpecifier.name.text); - const namedImport = this.createHoistableVariableDeclarationStatement( - importSpecifier.name, - tstl.createTableIndexExpression(importUniqueName, name), - importSpecifier - ); - result.push(namedImport); - } - }); - return result; - } else if (ts.isNamespaceImport(imports)) { - const requireStatement = this.createHoistableVariableDeclarationStatement( - imports.name, - requireCall, - statement - ); - result.push(requireStatement); - return result; - } else { - throw TSTLErrors.UnsupportedImportType(imports); - } - } - - private createModuleRequire(moduleSpecifier: ts.StringLiteral): tstl.CallExpression { - const importPath = moduleSpecifier.text.replace(new RegExp("\"", "g"), ""); - const resolvedModuleSpecifier = tstl.createStringLiteral(this.getImportPath(importPath)); - - return tstl.createCallExpression(tstl.createIdentifier("require"), [resolvedModuleSpecifier]); - } - - public transformClassDeclaration( - statement: ts.ClassLikeDeclaration, - nameOverride?: tstl.Identifier - ): tstl.Statement[] - { - this.classStack.push(statement); - - let className = statement.name ? this.transformIdentifier(statement.name) : nameOverride; - if (!className) { - throw TSTLErrors.MissingClassName(statement); - } - - const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(statement), this.checker); - - // Find out if this class is extension of existing class - const isExtension = decorators.has(DecoratorKind.Extension); - - const isMetaExtension = decorators.has(DecoratorKind.MetaExtension); - - if (isExtension && isMetaExtension) { - throw TSTLErrors.InvalidExtensionMetaExtension(statement); - } - - if ((isExtension || isMetaExtension) && this.isIdentifierExported(className)) { - // Cannot export extension classes - throw TSTLErrors.InvalidExportsExtension(statement); - } - - // Get type that is extended - const extendsType = tsHelper.getExtendedType(statement, this.checker); - - if (!(isExtension || isMetaExtension) && extendsType) { - // Non-extensions cannot extend extension classes - const extendsDecorators = tsHelper.getCustomDecorators(extendsType, this.checker); - if (extendsDecorators.has(DecoratorKind.Extension) || extendsDecorators.has(DecoratorKind.MetaExtension)) { - throw TSTLErrors.InvalidExtendsExtension(statement); - } - } - - // Get all properties with value - const properties = statement.members.filter(ts.isPropertyDeclaration).filter(member => member.initializer); - - // Divide properties into static and non-static - const staticFields = properties.filter(tsHelper.isStatic); - const instanceFields = properties.filter(prop => !tsHelper.isStatic(prop)); - - const result: tstl.Statement[] = []; - - // Overwrite the original className with the class we are overriding for extensions - if (isMetaExtension) { - if (!extendsType) { - throw TSTLErrors.MissingMetaExtension(statement); - } - - const extendsName = tstl.createStringLiteral(extendsType.symbol.escapedName as string); - className = tstl.createIdentifier("__meta__" + extendsName.value); - - // local className = debug.getregistry()["extendsName"] - const assignDebugCallIndex = tstl.createVariableDeclarationStatement( - className, - tstl.createTableIndexExpression( - tstl.createCallExpression( - tstl.createTableIndexExpression( - tstl.createIdentifier("debug"), - tstl.createStringLiteral("getregistry") - ), - [] - ), - extendsName), - statement); - - result.push(assignDebugCallIndex); - } - - if (isExtension) { - const extensionNameArg = decorators.get(DecoratorKind.Extension).args[0]; - if (extensionNameArg) { - className = tstl.createIdentifier(extensionNameArg); - } else if (extendsType) { - className = tstl.createIdentifier(extendsType.symbol.escapedName as string); - } - } - - if (!isExtension && !isMetaExtension) { - const classCreationMethods = this.createClassCreationMethods( - statement, - className, - extendsType - ); - result.push(...classCreationMethods); - } else { - for (const f of instanceFields) { - const fieldName = this.transformPropertyName(f.name); - - const value = this.transformExpression(f.initializer); - - // className["fieldName"] - const classField = tstl.createTableIndexExpression( - tstl.cloneIdentifier(className), - fieldName); - - // className["fieldName"] = value; - const assignClassField = tstl.createAssignmentStatement(classField, value); - - result.push(assignClassField); - } - } - - // Add static declarations - for (const field of staticFields) { - const fieldName = this.transformPropertyName(field.name); - const value = this.transformExpression(field.initializer); - - const classField = tstl.createTableIndexExpression( - this.addExportToIdentifier(tstl.cloneIdentifier(className)), - fieldName - ); - - const fieldAssign = tstl.createAssignmentStatement( - classField, - value - ); - - result.push(fieldAssign); - } - - // Find first constructor with body - if (!isExtension && !isMetaExtension) { - const constructor = statement.members - .filter(n => ts.isConstructorDeclaration(n) && n.body)[0] as ts.ConstructorDeclaration; - if (constructor) { - // Add constructor plus initialization of instance fields - result.push(this.transformConstructor(constructor, className, instanceFields, statement)); - } else if (!extendsType) { - // Generate a constructor if none was defined in a base class - result.push(this.transformConstructor( - ts.createConstructor([], [], [], ts.createBlock([], true)), - className, - instanceFields, - statement - )); - } else if (instanceFields.length > 0 - || statement.members.some(m => tsHelper.isGetAccessorOverride(m, statement, this.checker))) - { - // Generate a constructor if none was defined in a class with instance fields that need initialization - // className.prototype.____constructor = function(self, ...) - // baseClassName.prototype.____constructor(self, ...) - // ... - const constructorBody = this.transformClassInstanceFields(statement, instanceFields); - const superCall = tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createTableIndexExpression( - this.transformSuperKeyword(ts.createSuper()), - tstl.createStringLiteral("____constructor") - ), - [this.createSelfIdentifier(), tstl.createDotsLiteral()] - ) - ); - constructorBody.unshift(superCall); - const constructorFunction = tstl.createFunctionExpression( - tstl.createBlock(constructorBody), - [this.createSelfIdentifier()], - tstl.createDotsLiteral() - ); - result.push(tstl.createAssignmentStatement( - this.createConstructorName(className), - constructorFunction, - statement - )); - } - } - - // Transform get accessors - statement.members.filter(ts.isGetAccessor).forEach(getAccessor => { - result.push(this.transformGetAccessorDeclaration(getAccessor, className, statement)); - }); - - // Transform set accessors - statement.members.filter(ts.isSetAccessor).forEach(setAccessor => { - result.push(this.transformSetAccessorDeclaration(setAccessor, className, statement)); - }); - - // Transform methods - statement.members.filter(ts.isMethodDeclaration).forEach(method => { - result.push(this.transformMethodDeclaration(method, className, isExtension || isMetaExtension)); - }); - - this.classStack.pop(); - - return result; - } - - public createClassCreationMethods( - statement: ts.ClassLikeDeclarationBase, - className: tstl.Identifier, - extendsType: ts.Type - ): tstl.Statement[] - { - let noClassOr = false; - if (extendsType) { - const decorators = tsHelper.getCustomDecorators(extendsType, this.checker); - noClassOr = decorators.has(DecoratorKind.NoClassOr); - } - - const result: tstl.Statement[] = []; - - // className = className or {} - let classTable: tstl.Expression = tstl.createTableExpression(); - if (!noClassOr) { - classTable = tstl.createBinaryExpression( - this.addExportToIdentifier(className), // Use original identifier node in declaration - classTable, - tstl.SyntaxKind.OrOperator - ); - } - - const classVar = this.createLocalOrExportedOrGlobalDeclaration(className, classTable, statement); - result.push(...classVar); - - const createClassNameWithExport = () => this.addExportToIdentifier(tstl.cloneIdentifier(className)); - - // className.____getters = {} - if (statement.members.some(m => ts.isGetAccessor(m) && tsHelper.isStatic(m))) { - const classGetters = tstl.createTableIndexExpression( - createClassNameWithExport(), - tstl.createStringLiteral("____getters") - ); - const assignClassGetters = tstl.createAssignmentStatement( - classGetters, - tstl.createTableExpression(), - statement - ); - result.push(assignClassGetters); - - this.importLuaLibFeature(LuaLibFeature.ClassIndex); - } - - // className.__index = className - const classIndex = tstl.createTableIndexExpression( - createClassNameWithExport(), - tstl.createStringLiteral("__index") - ); - const assignClassIndex = tstl.createAssignmentStatement(classIndex, createClassNameWithExport(), statement); - result.push(assignClassIndex); - - // className.____setters = {} - if (statement.members.some(m => ts.isSetAccessor(m) && tsHelper.isStatic(m))) { - const classSetters = tstl.createTableIndexExpression( - createClassNameWithExport(), - tstl.createStringLiteral("____setters") - ); - const assignClassSetters = tstl.createAssignmentStatement( - classSetters, - tstl.createTableExpression(), - statement - ); - result.push(assignClassSetters); - - this.importLuaLibFeature(LuaLibFeature.ClassNewIndex); - } - - // className.prototype = className.prototype or {} - const createClassPrototype = () => tstl.createTableIndexExpression( - createClassNameWithExport(), - tstl.createStringLiteral("prototype") - ); - const classPrototypeTable = noClassOr - ? tstl.createTableExpression() - : tstl.createBinaryExpression( - createClassPrototype(), - tstl.createTableExpression(), - tstl.SyntaxKind.OrOperator - ); - const assignClassPrototype = tstl.createAssignmentStatement(createClassPrototype(), classPrototypeTable); - result.push(assignClassPrototype); - - // className.prototype.____getters = {} - if (statement.members.some(m => ts.isGetAccessor(m) && !tsHelper.isStatic(m))) { - const classPrototypeGetters = tstl.createTableIndexExpression( - createClassPrototype(), - tstl.createStringLiteral("____getters") - ); - const assignClassPrototypeGetters = tstl.createAssignmentStatement( - classPrototypeGetters, - tstl.createTableExpression() - ); - result.push(assignClassPrototypeGetters); - } - - const classPrototypeIndex = tstl.createTableIndexExpression( - createClassPrototype(), - tstl.createStringLiteral("__index") - ); - if (tsHelper.hasGetAccessorInClassOrAncestor(statement, false, this.checker)) { - // className.prototype.__index = __TS_Index(className.prototype) - const assignClassPrototypeIndex = tstl.createAssignmentStatement( - classPrototypeIndex, - this.transformLuaLibFunction(LuaLibFeature.Index, undefined, createClassPrototype()) - ); - result.push(assignClassPrototypeIndex); - - } else { - // className.prototype.__index = className.prototype - const assignClassPrototypeIndex = tstl.createAssignmentStatement( - classPrototypeIndex, - createClassPrototype() - ); - result.push(assignClassPrototypeIndex); - } - - if (statement.members.some(m => ts.isSetAccessor(m) && !tsHelper.isStatic(m))) { - // className.prototype.____setters = {} - const classPrototypeSetters = tstl.createTableIndexExpression( - createClassPrototype(), - tstl.createStringLiteral("____setters") - ); - const assignClassPrototypeSetters = tstl.createAssignmentStatement( - classPrototypeSetters, - tstl.createTableExpression() - ); - result.push(assignClassPrototypeSetters); - } - - if (tsHelper.hasSetAccessorInClassOrAncestor(statement, false, this.checker)) { - // className.prototype.__newindex = __TS_NewIndex(className.prototype) - const classPrototypeNewIndex = tstl.createTableIndexExpression( - createClassPrototype(), - tstl.createStringLiteral("__newindex") - ); - const assignClassPrototypeIndex = tstl.createAssignmentStatement( - classPrototypeNewIndex, - this.transformLuaLibFunction(LuaLibFeature.NewIndex, undefined, createClassPrototype()) - ); - result.push(assignClassPrototypeIndex); - } - - // className.prototype.constructor = className - const classPrototypeConstructor = tstl.createTableIndexExpression( - createClassPrototype(), - tstl.createStringLiteral("constructor") - ); - const assignClassPrototypeConstructor = tstl.createAssignmentStatement( - classPrototypeConstructor, - createClassNameWithExport(), - statement - ); - result.push(assignClassPrototypeConstructor); - - const hasStaticGetters = tsHelper.hasGetAccessorInClassOrAncestor(statement, true, this.checker); - const hasStaticSetters = tsHelper.hasSetAccessorInClassOrAncestor(statement, true, this.checker); - - if (extendsType) { - const extendedTypeNode = tsHelper.getExtendedTypeNode(statement, this.checker); - const baseName = this.transformExpression(extendedTypeNode.expression); - - // className.____super = baseName - const createClassBase = () => tstl.createTableIndexExpression( - createClassNameWithExport(), - tstl.createStringLiteral("____super") - ); - const assignClassBase = tstl.createAssignmentStatement(createClassBase(), baseName, statement); - result.push(assignClassBase); - - if (hasStaticGetters || hasStaticSetters) { - const metatableFields: tstl.TableFieldExpression[] = []; - if (hasStaticGetters) { - // __index = __TS__ClassIndex - metatableFields.push( - tstl.createTableFieldExpression( - tstl.createIdentifier("__TS__ClassIndex"), - tstl.createStringLiteral("__index") - ) - ); - } else { - // __index = className.____super - metatableFields.push( - tstl.createTableFieldExpression(createClassBase(), tstl.createStringLiteral("__index")) - ); - } - - if (hasStaticSetters) { - // __newindex = __TS__ClassNewIndex - metatableFields.push( - tstl.createTableFieldExpression( - tstl.createIdentifier("__TS__ClassNewIndex"), - tstl.createStringLiteral("__newindex") - ) - ); - } - - const setClassMetatable = tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createIdentifier("setmetatable"), - [createClassNameWithExport(), tstl.createTableExpression(metatableFields)] - ) - ); - result.push(setClassMetatable); - - } else { - // setmetatable(className, className.____super) - const setClassMetatable = tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createIdentifier("setmetatable"), - [createClassNameWithExport(), createClassBase()] - ) - ); - result.push(setClassMetatable); - } - - // setmetatable(className.prototype, className.____super.prototype) - const basePrototype = tstl.createTableIndexExpression( - createClassBase(), - tstl.createStringLiteral("prototype") - ); - const setClassPrototypeMetatable = tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createIdentifier("setmetatable"), - [createClassPrototype(), basePrototype] - ) - ); - result.push(setClassPrototypeMetatable); - - } else if (hasStaticGetters || hasStaticSetters) { - const metatableFields: tstl.TableFieldExpression[] = []; - if (hasStaticGetters) { - // __index = __TS__ClassIndex - metatableFields.push( - tstl.createTableFieldExpression( - tstl.createIdentifier("__TS__ClassIndex"), - tstl.createStringLiteral("__index") - ) - ); - } - - if (hasStaticSetters) { - // __newindex = __TS__ClassNewIndex - metatableFields.push( - tstl.createTableFieldExpression( - tstl.createIdentifier("__TS__ClassNewIndex"), - tstl.createStringLiteral("__newindex") - ) - ); - } - - const setClassMetatable = tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createIdentifier("setmetatable"), - [createClassNameWithExport(), tstl.createTableExpression(metatableFields)] - ) - ); - result.push(setClassMetatable); - } - - const newFuncStatements: tstl.Statement[] = []; - - // local self = setmetatable({}, className.prototype) - const assignSelf = tstl.createVariableDeclarationStatement( - this.createSelfIdentifier(), - tstl.createCallExpression( - tstl.createIdentifier("setmetatable"), - [tstl.createTableExpression(), createClassPrototype()] - ) - ); - newFuncStatements.push(assignSelf); - - // self:____constructor(...) - const callConstructor = tstl.createExpressionStatement( - tstl.createMethodCallExpression( - this.createSelfIdentifier(), - tstl.createIdentifier("____constructor"), - [tstl.createDotsLiteral()] - ) - ); - newFuncStatements.push(callConstructor); - - // return self - const returnSelf = tstl.createReturnStatement([this.createSelfIdentifier()]); - newFuncStatements.push(returnSelf); - - // function className.new(construct, ...) ... end - // or function export.className.new(construct, ...) ... end - const newFunc = tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - createClassNameWithExport(), - tstl.createStringLiteral("new")), - tstl.createFunctionExpression( - tstl.createBlock(newFuncStatements), - undefined, - tstl.createDotsLiteral(), - undefined, - statement - ) - ); - result.push(newFunc); - - return result; - } - - public transformClassInstanceFields( - classDeclarataion: ts.ClassLikeDeclaration, - instanceFields: ts.PropertyDeclaration[] - ): tstl.Statement[] - { - const statements: tstl.Statement[] = []; - - for (const f of instanceFields) { - // Get identifier - const fieldName = this.transformPropertyName(f.name); - - const value = this.transformExpression(f.initializer); - - // self[fieldName] - const selfIndex = tstl.createTableIndexExpression(this.createSelfIdentifier(), fieldName); - - // self[fieldName] = value - const assignClassField = tstl.createAssignmentStatement(selfIndex, value); - - statements.push(assignClassField); - } - - const getOverrides = classDeclarataion.members.filter( - m => tsHelper.isGetAccessorOverride(m, classDeclarataion, this.checker) - ); - for (const getter of getOverrides) { - const resetGetter = tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createIdentifier("rawset"), - [this.createSelfIdentifier(), this.transformPropertyName(getter.name), tstl.createNilLiteral()] - ) - ); - statements.push(resetGetter); - } - - return statements; - } - - public createConstructorName(className: tstl.Identifier): tstl.TableIndexExpression { - return tstl.createTableIndexExpression( - tstl.createTableIndexExpression( - this.addExportToIdentifier(tstl.cloneIdentifier(className)), - tstl.createStringLiteral("prototype") - ), - tstl.createStringLiteral("____constructor") - ); - } - - public transformConstructor( - statement: ts.ConstructorDeclaration, - className: tstl.Identifier, - instanceFields: ts.PropertyDeclaration[], - classDeclaration: ts.ClassLikeDeclaration - ): tstl.AssignmentStatement - { - // Don't transform methods without body (overload declarations) - if (!statement.body) { - return undefined; - } - - const bodyStatements: tstl.Statement[] = this.transformClassInstanceFields(classDeclaration, instanceFields); - - // Check for field declarations in constructor - const constructorFieldsDeclarations = statement.parameters.filter(p => p.modifiers !== undefined); - - // Add in instance field declarations - for (const declaration of constructorFieldsDeclarations) { - const declarationName = this.transformIdentifier(declaration.name as ts.Identifier); - if (declaration.initializer) { - // self.declarationName = declarationName or initializer - const assignement = tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - this.createSelfIdentifier(), tstl.createStringLiteral(declarationName.text) - ), - tstl.createBinaryExpression( - declarationName, - this.transformExpression(declaration.initializer), tstl.SyntaxKind.OrOperator - ) - ); - bodyStatements.push(assignement); - } else { - // self.declarationName = declarationName - const assignement = tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - this.createSelfIdentifier(), - tstl.createStringLiteral(declarationName.text) - ), - declarationName - ); - bodyStatements.push(assignement); - } - } - - // function className.constructor(self, params) ... end - - const [params, dotsLiteral, restParamName] = this.transformParameters( - statement.parameters, - this.createSelfIdentifier() - ); - - const [body] = this.transformFunctionBody(statement.parameters, statement.body, restParamName); - bodyStatements.push(...body); - - const block: tstl.Block = tstl.createBlock(bodyStatements); - - const result = tstl.createAssignmentStatement( - this.createConstructorName(className), - tstl.createFunctionExpression(block, params, dotsLiteral, restParamName, undefined, undefined), - statement - ); - - return result; - } - - public transformGetAccessorDeclaration( - getAccessor: ts.GetAccessorDeclaration, - className: tstl.Identifier, - classDeclaration: ts.ClassLikeDeclaration - ): tstl.AssignmentStatement - { - const name = this.transformIdentifier(getAccessor.name as ts.Identifier); - - const [body] = this.transformFunctionBody(getAccessor.parameters, getAccessor.body); - const accessorFunction = tstl.createFunctionExpression( - tstl.createBlock(body), - [this.createSelfIdentifier()] - ); - - const classNameWithExport = this.addExportToIdentifier(tstl.cloneIdentifier(className)); - const methodTable = tsHelper.isStatic(getAccessor) - ? classNameWithExport - : tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("prototype")); - - const classGetters = tstl.createTableIndexExpression( - methodTable, - tstl.createStringLiteral("____getters") - ); - const getter = tstl.createTableIndexExpression( - classGetters, - tstl.createStringLiteral(name.text) - ); - const assignGetter = tstl.createAssignmentStatement(getter, accessorFunction); - return assignGetter; - } - - public transformSetAccessorDeclaration( - setAccessor: ts.SetAccessorDeclaration, - className: tstl.Identifier, - classDeclaration: ts.ClassLikeDeclaration - ): tstl.AssignmentStatement - { - const name = this.transformIdentifier(setAccessor.name as ts.Identifier); - - const [params, dot, restParam] = this.transformParameters(setAccessor.parameters, this.createSelfIdentifier()); - - const [body] = this.transformFunctionBody(setAccessor.parameters, setAccessor.body, restParam); - const accessorFunction = tstl.createFunctionExpression( - tstl.createBlock(body), - params, - dot, - restParam - ); - - const classNameWithExport = this.addExportToIdentifier(tstl.cloneIdentifier(className)); - const methodTable = tsHelper.isStatic(setAccessor) - ? classNameWithExport - : tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("prototype")); - - const classSetters = tstl.createTableIndexExpression( - methodTable, - tstl.createStringLiteral("____setters") - ); - const setter = tstl.createTableIndexExpression( - classSetters, - tstl.createStringLiteral(name.text) - ); - const assignSetter = tstl.createAssignmentStatement(setter, accessorFunction); - return assignSetter; - } - - public transformMethodDeclaration( - node: ts.MethodDeclaration, - className: tstl.Identifier, - noPrototype: boolean - ): tstl.AssignmentStatement - { - // Don't transform methods without body (overload declarations) - if (!node.body) { - return undefined; - } - - let methodName = this.transformPropertyName(node.name); - if (tstl.isStringLiteral(methodName) && methodName.value === "toString") { - methodName = tstl.createStringLiteral("__tostring", node.name); - } - - const type = this.checker.getTypeAtLocation(node); - const context = tsHelper.getFunctionContextType(type, this.checker) !== ContextType.Void - ? this.createSelfIdentifier() - : undefined; - const [paramNames, dots, restParamName] = this.transformParameters(node.parameters, context); - - const [body] = this.transformFunctionBody(node.parameters, node.body, restParamName); - const functionExpression = tstl.createFunctionExpression( - tstl.createBlock(body), - paramNames, - dots, - restParamName - ); - - const classNameWithExport = this.addExportToIdentifier(tstl.cloneIdentifier(className)); - const methodTable = tsHelper.isStatic(node) || noPrototype - ? classNameWithExport - : tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("prototype")); - - return tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - methodTable, - methodName), - functionExpression, - node - ); - } - - public transformParameters(parameters: ts.NodeArray, context?: tstl.Identifier): - [tstl.Identifier[], tstl.DotsLiteral, tstl.Identifier | undefined] { - // Build parameter string - const paramNames: tstl.Identifier[] = []; - if (context) { - paramNames.push(context); - } - - let restParamName: tstl.Identifier; - let dotsLiteral: tstl.DotsLiteral; - let identifierIndex = 0; - - // Only push parameter name to paramName array if it isn't a spread parameter - for (const param of parameters) { - if (ts.isIdentifier(param.name) && param.name.originalKeywordKind === ts.SyntaxKind.ThisKeyword) { - continue; - } - - // Binding patterns become ____TS_bindingPattern0, ____TS_bindingPattern1, etc as function parameters - // See transformFunctionBody for how these values are destructured - const paramName = ts.isObjectBindingPattern(param.name) || ts.isArrayBindingPattern(param.name) - ? tstl.createIdentifier(`____TS_bindingPattern${identifierIndex++}`) - : this.transformIdentifier(param.name as ts.Identifier); - - // This parameter is a spread parameter (...param) - if (!param.dotDotDotToken) { - paramNames.push(paramName); - } else { - restParamName = paramName; - // Push the spread operator into the paramNames array - dotsLiteral = tstl.createDotsLiteral(); - } - } - - return [paramNames, dotsLiteral, restParamName]; - } - - public transformFunctionBody( - parameters: ts.NodeArray, - body: ts.Block, - spreadIdentifier?: tstl.Identifier - ): [tstl.Statement[], Scope] - { - this.pushScope(ScopeType.Function, body); - - const headerStatements = []; - - // Add default parameters - const defaultValueDeclarations = parameters - .filter(declaration => declaration.initializer !== undefined) - .map(declaration => this.transformParameterDefaultValueDeclaration(declaration)); - - headerStatements.push(...defaultValueDeclarations); - - // Add object binding patterns - let identifierIndex = 0; - const bindingPatternDeclarations: tstl.Statement[] = []; - parameters.forEach(binding => { - if (ts.isObjectBindingPattern(binding.name) || ts.isArrayBindingPattern(binding.name)) { - const identifier = tstl.createIdentifier(`____TS_bindingPattern${identifierIndex++}`); - bindingPatternDeclarations.push(...this.transformBindingPattern(binding.name, identifier)); - } - }); - - headerStatements.push(...bindingPatternDeclarations); - - // Push spread operator here - if (spreadIdentifier) { - const spreadTable = this.wrapInTable(tstl.createDotsLiteral()); - headerStatements.push(tstl.createVariableDeclarationStatement(spreadIdentifier, spreadTable)); - } - - const bodyStatements = this.performHoisting(this.transformStatements(body.statements)); - - const scope = this.popScope(); - - return [headerStatements.concat(bodyStatements), scope]; - } - - public transformParameterDefaultValueDeclaration(declaration: ts.ParameterDeclaration): tstl.Statement { - const parameterName = this.transformIdentifier(declaration.name as ts.Identifier); - const parameterValue = this.transformExpression(declaration.initializer); - const assignment = tstl.createAssignmentStatement(parameterName, parameterValue); - - const nilCondition = tstl.createBinaryExpression( - parameterName, - tstl.createNilLiteral(), - tstl.SyntaxKind.EqualityOperator - ); - - const ifBlock = tstl.createBlock([assignment]); - - return tstl.createIfStatement(nilCondition, ifBlock, undefined, declaration); - } - - public * transformBindingPattern( - pattern: ts.BindingPattern, - table: tstl.Identifier, - propertyAccessStack: ts.PropertyName[] = [] - ): IterableIterator - { - const isObjectBindingPattern = ts.isObjectBindingPattern(pattern); - for (let index = 0; index < pattern.elements.length; index++) { - const element = pattern.elements[index]; - if (ts.isBindingElement(element)) { - if (ts.isArrayBindingPattern(element.name) || ts.isObjectBindingPattern(element.name)) { - // nested binding pattern - const propertyName = isObjectBindingPattern - ? element.propertyName - : ts.createNumericLiteral(String(index + 1)); - propertyAccessStack.push(propertyName); - yield* this.transformBindingPattern(element.name, table, propertyAccessStack); - } else { - // Disallow ellipsis destructure - if (element.dotDotDotToken) { - throw TSTLErrors.ForbiddenEllipsisDestruction(element); - } - // Build the path to the table - let tableExpression: tstl.Expression = table; - propertyAccessStack.forEach(property => { - const propertyName = ts.isPropertyName(property) - ? this.transformPropertyName(property) - : this.transformNumericLiteral(property); - tableExpression = tstl.createTableIndexExpression(tableExpression, propertyName); - }); - // The identifier of the new variable - const variableName = this.transformIdentifier(element.name as ts.Identifier); - // The field to extract - const propertyName = this.transformIdentifier( - (element.propertyName || element.name) as ts.Identifier); - const expression = isObjectBindingPattern - ? tstl.createTableIndexExpression(tableExpression, tstl.createStringLiteral(propertyName.text)) - : tstl.createTableIndexExpression(tableExpression, tstl.createNumericLiteral(index + 1)); - if (element.initializer) { - const defaultExpression = tstl.createBinaryExpression(expression, - this.transformExpression(element.initializer), tstl.SyntaxKind.OrOperator); - yield* this.createLocalOrExportedOrGlobalDeclaration(variableName, defaultExpression); - } else { - yield* this.createLocalOrExportedOrGlobalDeclaration(variableName, expression); - } - } - } - } - propertyAccessStack.pop(); - } - - public transformModuleDeclaration(statement: ts.ModuleDeclaration): tstl.Statement[] { - const decorators = tsHelper.getCustomDecorators(this.checker.getTypeAtLocation(statement), this.checker); - // If phantom namespace elide the declaration and return the body - if (decorators.has(DecoratorKind.Phantom) && statement.body && ts.isModuleBlock(statement.body)) { - return this.transformStatements(statement.body.statements); - } - - const result: tstl.Statement[] = []; - - if (this.currentNamespace) { - // outerNS.innerNS = outerNS.innerNS or {} - const namespaceDeclaration = tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - this.transformIdentifier(this.currentNamespace.name as ts.Identifier), - tstl.createStringLiteral(this.transformIdentifier(statement.name as ts.Identifier).text)), - tstl.createBinaryExpression( - tstl.createTableIndexExpression( - this.transformIdentifier(this.currentNamespace.name as ts.Identifier), - tstl.createStringLiteral(this.transformIdentifier(statement.name as ts.Identifier).text)), - tstl.createTableExpression(), - tstl.SyntaxKind.OrOperator)); - - result.push(namespaceDeclaration); - - // local innerNS = outerNS.innerNS - const localDeclaration = this.createHoistableVariableDeclarationStatement( - statement.name as ts.Identifier, - tstl.createTableIndexExpression( - this.transformIdentifier(this.currentNamespace.name as ts.Identifier), - tstl.createStringLiteral(this.transformIdentifier(statement.name as ts.Identifier).text))); - - result.push(localDeclaration); - - } else if (this.isModule && (ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export)) { - // exports.NS = exports.NS or {} - const namespaceDeclaration = tstl.createAssignmentStatement( - this.createExportedIdentifier(this.transformIdentifier(statement.name as ts.Identifier)), - tstl.createBinaryExpression( - this.createExportedIdentifier(this.transformIdentifier(statement.name as ts.Identifier)), - tstl.createTableExpression(), - tstl.SyntaxKind.OrOperator)); - - result.push(namespaceDeclaration); - - // local NS = exports.NS - const localDeclaration = this.createHoistableVariableDeclarationStatement( - statement.name as ts.Identifier, - this.createExportedIdentifier(this.transformIdentifier(statement.name as ts.Identifier))); - - result.push(localDeclaration); - - } else { - // local NS = NS or {} - const localDeclaration = this.createLocalOrExportedOrGlobalDeclaration( - this.transformIdentifier(statement.name as ts.Identifier), - tstl.createBinaryExpression( - this.transformIdentifier(statement.name as ts.Identifier), - tstl.createTableExpression(), - tstl.SyntaxKind.OrOperator - ) - ); - - result.push(...localDeclaration); - } - - // Set current namespace for nested NS - // Keep previous currentNS to reset after block transpilation - const previousNamespace = this.currentNamespace; - this.currentNamespace = statement; - - // Transform moduleblock to block and visit it - if (statement.body && (ts.isModuleBlock(statement.body) || ts.isModuleDeclaration(statement.body))) { - this.pushScope(ScopeType.Block, statement); - let statements = ts.isModuleBlock(statement.body) - ? this.transformStatements(statement.body.statements) - : this.transformModuleDeclaration(statement.body); - statements = this.performHoisting(statements); - this.popScope(); - result.push(tstl.createDoStatement(statements)); - } - - this.currentNamespace = previousNamespace; - - return result; - } - - public transformEnumDeclaration(enumDeclaration: ts.EnumDeclaration): StatementVisitResult { - const type = this.checker.getTypeAtLocation(enumDeclaration); - - // Const enums should never appear in the resulting code - if (type.symbol.getFlags() & ts.SymbolFlags.ConstEnum) { - return undefined; - } - - const membersOnly = tsHelper.getCustomDecorators(type, this.checker).has(DecoratorKind.CompileMembersOnly); - - const result: tstl.Statement[] = []; - - if (!membersOnly) { - const name = this.transformIdentifier(enumDeclaration.name); - const table = tstl.createTableExpression(); - result.push(...this.createLocalOrExportedOrGlobalDeclaration(name, table, enumDeclaration)); - } - - for (const enumMember of this.computeEnumMembers(enumDeclaration)) { - const memberName = this.transformPropertyName(enumMember.name); - if (membersOnly) { - if (tstl.isIdentifier(memberName)) { - result.push(...this.createLocalOrExportedOrGlobalDeclaration( - memberName, - enumMember.value, - enumDeclaration - )); - } else { - result.push(...this.createLocalOrExportedOrGlobalDeclaration( - tstl.createIdentifier(enumMember.name.getText(), enumMember.name), - enumMember.value, - enumDeclaration - )); - } - } else { - const enumTable = this.transformIdentifierExpression(enumDeclaration.name); - const property = tstl.createTableIndexExpression(enumTable, memberName); - result.push(tstl.createAssignmentStatement(property, enumMember.value, enumMember.original)); - - const valueIndex = tstl.createTableIndexExpression(enumTable, enumMember.value); - result.push(tstl.createAssignmentStatement(valueIndex, memberName, enumMember.original)); - } - } - - return result; - } - - public computeEnumMembers(node: ts.EnumDeclaration): - Array<{name: ts.PropertyName, value: tstl.Expression, original: ts.Node}> { - let numericValue = 0; - let hasStringInitializers = false; - - const valueMap = new Map(); - - return node.members.map(member => { - let valueExpression: tstl.Expression; - if (member.initializer) { - if (ts.isNumericLiteral(member.initializer)) - { - numericValue = Number(member.initializer.text); - valueExpression = this.transformNumericLiteral(member.initializer); - numericValue++; - } - else if (ts.isStringLiteral(member.initializer)) - { - hasStringInitializers = true; - valueExpression = this.transformStringLiteral(member.initializer); - } - else - { - if (ts.isIdentifier(member.initializer)) { - const [isEnumMember, originalName] = tsHelper.isEnumMember(node, member.initializer); - if (isEnumMember) { - valueExpression = valueMap.get(originalName); - } else { - valueExpression = this.transformExpression(member.initializer); - } - } else { - valueExpression = this.transformExpression(member.initializer); - } - } - } - else if (hasStringInitializers) - { - throw TSTLErrors.HeterogeneousEnum(node); - } - else - { - valueExpression = tstl.createNumericLiteral(numericValue); - numericValue++; - } - - valueMap.set(member.name, valueExpression); - - const enumMember = { - name: member.name, - original: member, - value: valueExpression, - }; - - return enumMember; - }); - } - - private transformGeneratorFunction( - parameters: ts.NodeArray, - body: ts.Block, - transformedParameters: tstl.Identifier[], - dotsLiteral: tstl.DotsLiteral, - spreadIdentifier?: tstl.Identifier - ): [tstl.Statement[], Scope] - { - this.importLuaLibFeature(LuaLibFeature.Symbol); - const [functionBody, functionScope] = this.transformFunctionBody( - parameters, - body, - spreadIdentifier - ); - - const coroutineIdentifier = tstl.createIdentifier("____co"); - const valueIdentifier = tstl.createIdentifier("____value"); - const errIdentifier = tstl.createIdentifier("____err"); - const itIdentifier = tstl.createIdentifier("____it"); - - //local ____co = coroutine.create(originalFunction) - const coroutine = - tstl.createVariableDeclarationStatement(coroutineIdentifier, - tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier("coroutine"), - tstl.createStringLiteral("create") - ), - [tstl.createFunctionExpression( - tstl.createBlock(functionBody), - transformedParameters, - dotsLiteral, - spreadIdentifier), - ] - ) - ); - - const nextBody = []; - // coroutine.resume(__co, ...) - const resumeCall = tstl.createCallExpression( - tstl.createTableIndexExpression( - tstl.createIdentifier("coroutine"), - tstl.createStringLiteral("resume") - ), - [coroutineIdentifier, tstl.createDotsLiteral()] - ); - - // ____err, ____value = coroutine.resume(____co, ...) - nextBody.push(tstl.createVariableDeclarationStatement( - [errIdentifier, valueIdentifier], - resumeCall) - ); - - //coroutine.status(____co) ~= "dead"; - const coStatus = tstl.createCallExpression( - tstl.createTableIndexExpression( - tstl.createIdentifier("coroutine"), - tstl.createStringLiteral("status") - ), - [coroutineIdentifier] - ); - const status = tstl.createBinaryExpression( - coStatus, - tstl.createStringLiteral("dead"), - tstl.SyntaxKind.EqualityOperator - ); - nextBody.push(status); - //if(not ____err){error(____value)} - const errorCheck = tstl.createIfStatement( - tstl.createUnaryExpression( - errIdentifier, - tstl.SyntaxKind.NotOperator - ), - tstl.createBlock([ - tstl.createExpressionStatement( - tstl.createCallExpression( - tstl.createIdentifier("error"), - [valueIdentifier] - ) - ), - ]) - ); - nextBody.push(errorCheck); - //{done = coroutine.status(____co) ~= "dead"; value = ____value} - const iteratorResult = tstl.createTableExpression([ - tstl.createTableFieldExpression( - status, - tstl.createStringLiteral("done") - ), - tstl.createTableFieldExpression( - valueIdentifier, - tstl.createStringLiteral("value") - ), - ]); - nextBody.push(tstl.createReturnStatement([iteratorResult])); - - //function(____, ...) - const nextFunctionDeclaration = tstl.createFunctionExpression( - tstl.createBlock(nextBody), - [tstl.createAnnonymousIdentifier()], - tstl.createDotsLiteral()); - - //____it = {next = function(____, ...)} - const iterator = tstl.createVariableDeclarationStatement( - itIdentifier, - tstl.createTableExpression([ - tstl.createTableFieldExpression( - nextFunctionDeclaration, - tstl.createStringLiteral("next") - ), - ]) - ); - - const symbolIterator = tstl.createTableIndexExpression( - tstl.createIdentifier("Symbol"), - tstl.createStringLiteral("iterator") - ); - - const block = [ - coroutine, - iterator, - //____it[Symbol.iterator] = {return ____it} - tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - itIdentifier, - symbolIterator - ), - tstl.createFunctionExpression( - tstl.createBlock( - [tstl.createReturnStatement([itIdentifier])] - ) - ) - ), - //return ____it - tstl.createReturnStatement([itIdentifier]), - ]; - return [block, functionScope]; - } - - public transformFunctionDeclaration(functionDeclaration: ts.FunctionDeclaration): StatementVisitResult { - // Don't transform functions without body (overload declarations) - if (!functionDeclaration.body) { - return undefined; - } - - const type = this.checker.getTypeAtLocation(functionDeclaration); - const context = tsHelper.getFunctionContextType(type, this.checker) !== ContextType.Void - ? this.createSelfIdentifier() - : undefined; - const [params, dotsLiteral, restParamName] = this.transformParameters(functionDeclaration.parameters, context); - - const name = this.transformIdentifier(functionDeclaration.name); - const [body, functionScope] = functionDeclaration.asteriskToken - ? this.transformGeneratorFunction( - functionDeclaration.parameters, - functionDeclaration.body, - params, - dotsLiteral, - restParamName - ) - : this.transformFunctionBody( - functionDeclaration.parameters, - functionDeclaration.body, - restParamName - ); - const block = tstl.createBlock(body); - const functionExpression = tstl.createFunctionExpression(block, params, dotsLiteral, restParamName); - // Remember symbols referenced in this function for hoisting later - if (!this.options.noHoisting && name.symbolId !== undefined) { - const scope = this.peekScope(); - if (!scope.functionDefinitions) { scope.functionDefinitions = new Map(); } - const functionInfo = {referencedSymbols: functionScope.referencedSymbols || new Set()}; - scope.functionDefinitions.set(name.symbolId, functionInfo); - } - return this.createLocalOrExportedOrGlobalDeclaration(name, functionExpression, functionDeclaration); - } - - public transformTypeAliasDeclaration(statement: ts.TypeAliasDeclaration): undefined { - return undefined; - } - - public transformInterfaceDeclaration(statement: ts.InterfaceDeclaration): undefined { - return undefined; - } - - public transformVariableDeclaration(statement: ts.VariableDeclaration) - : tstl.Statement[] - { - if (statement.initializer) { - // Validate assignment - const initializerType = this.checker.getTypeAtLocation(statement.initializer); - const varType = this.checker.getTypeFromTypeNode(statement.type); - this.validateFunctionAssignment(statement.initializer, initializerType, varType); - } - - if (ts.isIdentifier(statement.name)) { - // Find variable identifier - const identifierName = this.transformIdentifier(statement.name); - if (statement.initializer) { - const value = this.transformExpression(statement.initializer); - return this.createLocalOrExportedOrGlobalDeclaration(identifierName, value, statement); - } else { - return this.createLocalOrExportedOrGlobalDeclaration( - identifierName, - undefined, - statement - ); - } - } else if (ts.isArrayBindingPattern(statement.name) || ts.isObjectBindingPattern(statement.name)) { - // Destructuring types - - // For nested bindings and object bindings, fall back to transformBindingPattern - if (ts.isObjectBindingPattern(statement.name) - || statement.name.elements.some(elem => !ts.isBindingElement(elem) || !ts.isIdentifier(elem.name))) { - const statements = []; - let table: tstl.Identifier; - if (ts.isIdentifier(statement.initializer)) { - table = this.transformIdentifier(statement.initializer); - } else { - // Contain the expression in a temporary variable - table = tstl.createIdentifier("____"); - statements.push(tstl.createVariableDeclarationStatement( - table, this.transformExpression(statement.initializer))); - } - statements.push(...this.transformBindingPattern(statement.name, table)); - return statements; - } - - // Disallow ellipsis destruction - if (statement.name.elements.some(elem => !ts.isBindingElement(elem) || elem.dotDotDotToken !== undefined)) { - throw TSTLErrors.ForbiddenEllipsisDestruction(statement); - } - - const vars = statement.name.elements.map(e => this.transformArrayBindingElement(e)); - - // Don't unpack TupleReturn decorated functions - if (statement.initializer) { - if (tsHelper.isTupleReturnCall(statement.initializer, this.checker)) { - return this.createLocalOrExportedOrGlobalDeclaration( - vars, - this.transformExpression(statement.initializer), - statement - ); - } else { - // local vars = this.transpileDestructingAssignmentValue(node.initializer); - const initializer = this.createUnpackCall( - this.transformExpression(statement.initializer), - statement.initializer - ); - return this.createLocalOrExportedOrGlobalDeclaration(vars, initializer, statement); - } - } else { - return this.createLocalOrExportedOrGlobalDeclaration( - vars, - tstl.createNilLiteral(), - statement - ); - } - } - } - - public transformVariableStatement(statement: ts.VariableStatement): tstl.Statement[] { - const result: tstl.Statement[] = []; - statement.declarationList.declarations - .forEach(declaration => result.push(...this.transformVariableDeclaration(declaration))); - return result; - } - - public transformExpressionStatement(statement: ts.ExpressionStatement | ts.Expression): tstl.Statement { - const expression = ts.isExpressionStatement(statement) ? statement.expression : statement; - if (ts.isBinaryExpression(expression)) { - const [isCompound, replacementOperator] = tsHelper.isBinaryAssignmentToken(expression.operatorToken.kind); - if (isCompound) { - // +=, -=, etc... - return this.transformCompoundAssignmentStatement( - expression, - expression.left, - expression.right, - replacementOperator - ); - - } else if (expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - // = assignment - return this.transformAssignmentStatement(expression); - - } else if (expression.operatorToken.kind === ts.SyntaxKind.CommaToken) { - const lhs = this.transformExpressionStatement(expression.left); - const rhs = this.transformExpressionStatement(expression.right); - return tstl.createDoStatement([lhs, rhs], expression); - } - - } else if ( - ts.isPrefixUnaryExpression(expression) && - (expression.operator === ts.SyntaxKind.PlusPlusToken - || expression.operator === ts.SyntaxKind.MinusMinusToken)) { - // ++i, --i - const replacementOperator = expression.operator === ts.SyntaxKind.PlusPlusToken - ? tstl.SyntaxKind.AdditionOperator - : tstl.SyntaxKind.SubractionOperator; - - return this.transformCompoundAssignmentStatement( - expression, - expression.operand, - ts.createLiteral(1), - replacementOperator - ); - } - - else if (ts.isPostfixUnaryExpression(expression)) { - // i++, i-- - const replacementOperator = expression.operator === ts.SyntaxKind.PlusPlusToken - ? tstl.SyntaxKind.AdditionOperator - : tstl.SyntaxKind.SubractionOperator; - - return this.transformCompoundAssignmentStatement( - expression, - expression.operand, - ts.createLiteral(1), - replacementOperator - ); - } - - else if (ts.isDeleteExpression(expression)) { - return tstl.createAssignmentStatement( - this.transformExpression(expression.expression) as tstl.IdentifierOrTableIndexExpression, - tstl.createNilLiteral(), - expression - ); - } - - return tstl.createExpressionStatement(this.transformExpression(expression)); - } - - public transformYield(expression: ts.YieldExpression): tstl.Expression { - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier("coroutine"), tstl.createStringLiteral("yield")), - expression.expression?[this.transformExpression(expression.expression)]:[], expression); - } - - public transformReturn(statement: ts.ReturnStatement): tstl.Statement { - if (statement.expression) { - const returnType = tsHelper.getContainingFunctionReturnType(statement, this.checker); - if (returnType) { - const expressionType = this.checker.getTypeAtLocation(statement.expression); - this.validateFunctionAssignment(statement, expressionType, returnType); - } - if (tsHelper.isInTupleReturnFunction(statement, this.checker)) { - // Parent function is a TupleReturn function - if (ts.isArrayLiteralExpression(statement.expression)) { - // If return expression is an array literal, leave out brackets. - return tstl.createReturnStatement(statement.expression.elements - .map(elem => this.transformExpression(elem))); - } else if ( - !tsHelper.isTupleReturnCall(statement.expression, this.checker) - && !tsHelper.isInLuaIteratorFunction(statement, this.checker)) - { - // If return expression is not another TupleReturn call, unpack it - const expression = this.createUnpackCall( - this.transformExpression(statement.expression), - statement.expression - ); - return tstl.createReturnStatement([expression]); - } - } - return tstl.createReturnStatement([this.transformExpression(statement.expression)]); - } else { - // Empty return - return tstl.createReturnStatement(); - } - } - - public transformIfStatement(statement: ts.IfStatement): tstl.IfStatement { - this.pushScope(ScopeType.Conditional, statement.thenStatement); - const condition = this.transformExpression(statement.expression); - const statements = this.performHoisting(this.transformBlockOrStatement(statement.thenStatement)); - this.popScope(); - const ifBlock = tstl.createBlock(statements); - if (statement.elseStatement) { - if (ts.isIfStatement(statement.elseStatement)) { - return tstl.createIfStatement(condition, ifBlock, this.transformIfStatement(statement.elseStatement)); - } else { - this.pushScope(ScopeType.Conditional, statement.elseStatement); - const elseStatements = this.performHoisting(this.transformBlockOrStatement(statement.elseStatement)); - this.popScope(); - const elseBlock = tstl.createBlock(elseStatements); - return tstl.createIfStatement(condition, ifBlock, elseBlock); - } - } - return tstl.createIfStatement(condition, ifBlock); - } - - public transformWhileStatement(statement: ts.WhileStatement): tstl.WhileStatement { - return tstl.createWhileStatement( - tstl.createBlock(this.transformLoopBody(statement)), - this.transformExpression(statement.expression), - statement - ); - } - - public transformDoStatement(statement: ts.DoStatement): tstl.RepeatStatement { - return tstl.createRepeatStatement( - tstl.createBlock(this.transformLoopBody(statement)), - tstl.createUnaryExpression(this.transformExpression(statement.expression), tstl.SyntaxKind.NotOperator), - statement - ); - } - - public transformForStatement(statement: ts.ForStatement): tstl.DoStatement { - const result: tstl.Statement[] = []; - - if (statement.initializer) { - if (ts.isVariableDeclarationList(statement.initializer)) { - for (const variableDeclaration of statement.initializer.declarations) { - // local initializer = value - result.push(...this.transformVariableDeclaration(variableDeclaration)); - } - } else { - result.push(this.transformExpressionStatement(statement.initializer)); - } - } - - const condition = statement.condition - ? this.transformExpression(statement.condition) - : tstl.createBooleanLiteral(true); - - // Add body - const body: tstl.Statement[] = this.transformLoopBody(statement); - - if (statement.incrementor) { - body.push(this.transformExpressionStatement(statement.incrementor)); - } - - // while (condition) do ... end - result.push(tstl.createWhileStatement(tstl.createBlock(body), condition)); - - return tstl.createDoStatement(result, statement); - } - - public transformForOfInitializer(initializer: ts.ForInitializer, expression: tstl.Expression): tstl.Statement { - if (ts.isVariableDeclarationList(initializer)) { - // Declaration of new variable - const variableDeclarations = this.transformVariableDeclaration(initializer.declarations[0]); - if (ts.isArrayBindingPattern(initializer.declarations[0].name)) { - expression = this.createUnpackCall(expression, initializer); - } - // we can safely assume that for vars are not exported and therefore declarationstatenents - return tstl.createVariableDeclarationStatement( - (variableDeclarations[0] as tstl.VariableDeclarationStatement).left, expression); - - } else { - // Assignment to existing variable - let variables: tstl.IdentifierOrTableIndexExpression | tstl.IdentifierOrTableIndexExpression[]; - if (ts.isArrayLiteralExpression(initializer)) { - expression = this.createUnpackCall(expression, initializer); - variables = initializer.elements - .map(e => this.transformExpression(e)) as tstl.IdentifierOrTableIndexExpression[]; - } else { - variables = this.transformExpression(initializer) as tstl.IdentifierOrTableIndexExpression; - } - return tstl.createAssignmentStatement(variables, expression); - } - } - - public transformLoopBody( - loop: ts.WhileStatement | ts.DoStatement | ts.ForStatement | ts.ForOfStatement | ts.ForInOrOfStatement - ): tstl.Statement[] - { - this.pushScope(ScopeType.Loop, loop.statement); - const body = this.performHoisting(this.transformBlockOrStatement(loop.statement)); - const scope = this.popScope(); - const scopeId = scope.id; - - if (this.options.luaTarget === LuaTarget.Lua51) { - return body; - } - - const baseResult: tstl.Statement[] = [tstl.createDoStatement(body)]; - const continueLabel = tstl.createLabelStatement(`__continue${scopeId}`); - baseResult.push(continueLabel); - - return baseResult; - } - - public transformBlockOrStatement(statement: ts.Statement): tstl.Statement[] { - return ts.isBlock(statement) - ? this.transformStatements(statement.statements) - : this.statementVisitResultToStatementArray(this.transformStatement(statement)); - } - - public transformForOfArrayStatement(statement: ts.ForOfStatement, block: tstl.Block): StatementVisitResult { - const arrayExpression = this.transformExpression(statement.expression); - - // Arrays use numeric for loop (performs better than ipairs) - const indexVariable = tstl.createIdentifier("____TS_index"); - if (!ts.isIdentifier(statement.expression)) { - // Cache iterable expression if it's not a simple identifier - // local ____TS_array = ${iterable}; - // for ____TS_index = 1, #____TS_array do - // local ${initializer} = ____TS_array[____TS_index] - const arrayVariable = tstl.createIdentifier("____TS_array"); - const arrayAccess = tstl.createTableIndexExpression(arrayVariable, indexVariable); - const initializer = this.transformForOfInitializer(statement.initializer, arrayAccess); - block.statements.splice(0, 0, initializer); - return [ - tstl.createVariableDeclarationStatement(arrayVariable, arrayExpression), - tstl.createForStatement( - block, - indexVariable, - tstl.createNumericLiteral(1), - tstl.createUnaryExpression(arrayVariable, tstl.SyntaxKind.LengthOperator) - ), - ]; - - } else { - // Simple identifier version - // for ____TS_index = 1, #${iterable} do - // local ${initializer} = ${iterable}[____TS_index] - const iterableAccess = tstl.createTableIndexExpression(arrayExpression, indexVariable); - const initializer = this.transformForOfInitializer(statement.initializer, iterableAccess); - block.statements.splice(0, 0, initializer); - return tstl.createForStatement( - block, - indexVariable, - tstl.createNumericLiteral(1), - tstl.createUnaryExpression(arrayExpression, tstl.SyntaxKind.LengthOperator) - ); - } - } - - public transformForOfLuaIteratorStatement(statement: ts.ForOfStatement, block: tstl.Block): StatementVisitResult { - const luaIterator = this.transformExpression(statement.expression); - if (tsHelper.isTupleReturnCall(statement.expression, this.checker)) { - // LuaIterator + TupleReturn - if (ts.isVariableDeclarationList(statement.initializer)) { - // Variables declared in for loop - // for ${initializer} in ${iterable} do - const initializerVariable = statement.initializer.declarations[0].name; - if (ts.isArrayBindingPattern(initializerVariable)) { - return tstl.createForInStatement( - block, - initializerVariable.elements.map(e => this.transformArrayBindingElement(e)), - [luaIterator] - ); - - } else { - // Single variable is not allowed - throw TSTLErrors.UnsupportedNonDestructuringLuaIterator(statement.initializer); - } - - } else { - // Variables NOT declared in for loop - catch iterator values in temps and assign - // for ____TS_value0 in ${iterable} do - // ${initializer} = ____TS_value0 - if (ts.isArrayLiteralExpression(statement.initializer)) { - const tmps = statement.initializer.elements - .map((_, i) => tstl.createIdentifier(`____TS_value${i}`)); - const assign = tstl.createAssignmentStatement( - statement.initializer.elements - .map(e => this.transformExpression(e)) as tstl.IdentifierOrTableIndexExpression[], - tmps - ); - block.statements.splice(0, 0, assign); - return tstl.createForInStatement(block, tmps, [luaIterator]); - - } else { - // Single variable is not allowed - throw TSTLErrors.UnsupportedNonDestructuringLuaIterator(statement.initializer); - } - } - - } else { - // LuaIterator (no TupleReturn) - if (ts.isVariableDeclarationList(statement.initializer) - && ts.isIdentifier(statement.initializer.declarations[0].name)) { - // Single variable declared in for loop - // for ${initializer} in ${iterator} do - return tstl.createForInStatement( - block, - [this.transformIdentifier(statement.initializer.declarations[0].name as ts.Identifier)], - [luaIterator] - ); - - } else { - // Destructuring or variable NOT declared in for loop - // for ____TS_value in ${iterator} do - // local ${initializer} = unpack(____TS_value) - const valueVariable = tstl.createIdentifier("____TS_value"); - const initializer = this.transformForOfInitializer(statement.initializer, valueVariable); - block.statements.splice(0, 0, initializer); - return tstl.createForInStatement( - block, - [valueVariable], - [luaIterator] - ); - } - } - } - - public transformForOfIteratorStatement(statement: ts.ForOfStatement, block: tstl.Block): StatementVisitResult { - const iterable = this.transformExpression(statement.expression); - if (ts.isVariableDeclarationList(statement.initializer) - && ts.isIdentifier(statement.initializer.declarations[0].name)) { - // Single variable declared in for loop - // for ${initializer} in __TS__iterator(${iterator}) do - return tstl.createForInStatement( - block, - [this.transformIdentifier(statement.initializer.declarations[0].name as ts.Identifier)], - [this.transformLuaLibFunction(LuaLibFeature.Iterator, statement.expression, iterable)] - ); - - } else { - // Destructuring or variable NOT declared in for loop - // for ____TS_value in __TS__iterator(${iterator}) do - // local ${initializer} = ____TS_value - const valueVariable = tstl.createIdentifier("____TS_value"); - const initializer = this.transformForOfInitializer(statement.initializer, valueVariable); - block.statements.splice(0, 0, initializer); - return tstl.createForInStatement( - block, - [valueVariable], - [this.transformLuaLibFunction(LuaLibFeature.Iterator, statement.expression, iterable)] - ); - } - } - - public transformForOfStatement(statement: ts.ForOfStatement): StatementVisitResult { - // Transpile body - const body = tstl.createBlock(this.transformLoopBody(statement)); - - if (tsHelper.isArrayType(this.checker.getTypeAtLocation(statement.expression), this.checker)) { - // Arrays - return this.transformForOfArrayStatement(statement, body); - - } else if (tsHelper.isLuaIteratorCall(statement.expression, this.checker)) { - // LuaIterators - return this.transformForOfLuaIteratorStatement(statement, body); - - } else { - // TS Iterables - return this.transformForOfIteratorStatement(statement, body); - } - } - - public transformForInStatement(statement: ts.ForInStatement): StatementVisitResult { - // Get variable identifier - const variable = (statement.initializer as ts.VariableDeclarationList).declarations[0]; - const identifier = variable.name as ts.Identifier; - - // Transpile expression - const pairsIdentifier = tstl.createIdentifier("pairs"); - const expression = tstl.createCallExpression(pairsIdentifier, [this.transformExpression(statement.expression)]); - - if (tsHelper.isArrayType(this.checker.getTypeAtLocation(statement.expression), this.checker)) { - throw TSTLErrors.ForbiddenForIn(statement); - } - - const body = tstl.createBlock(this.transformLoopBody(statement)); - - return tstl.createForInStatement( - body, - [this.transformIdentifier(identifier)], - [expression], - statement - ); - } - - public transformSwitchStatement(statement: ts.SwitchStatement): StatementVisitResult { - if (this.options.luaTarget === LuaTarget.Lua51) { - throw TSTLErrors.UnsupportedForTarget("Switch statements", this.options.luaTarget, statement); - } - - this.pushScope(ScopeType.Switch, statement); - - // Give the switch a unique name to prevent nested switches from acting up. - const switchName = `____TS_switch${this.peekScope().id}`; - - const expression = this.transformExpression(statement.expression); - const switchVariable = tstl.createIdentifier(switchName); - const switchVariableDeclaration = tstl.createVariableDeclarationStatement(switchVariable, expression); - - let statements: tstl.Statement[] = [switchVariableDeclaration]; - - const caseClauses = statement.caseBlock.clauses.filter(c => ts.isCaseClause(c)) as ts.CaseClause[]; - - for (let i = 0; i < caseClauses.length; i++) { - const clause = caseClauses[i]; - // If the clause condition holds, go to the correct label - const condition = tstl.createBinaryExpression( - switchVariable, - this.transformExpression(clause.expression), - tstl.SyntaxKind.EqualityOperator - ); - const goto = tstl.createGotoStatement(`${switchName}_case_${i}`); - const conditionalGoto = tstl.createIfStatement(condition, tstl.createBlock([goto])); - statements.push(conditionalGoto); - } - - const hasDefaultCase = statement.caseBlock.clauses.some(c => ts.isDefaultClause(c)); - if (hasDefaultCase) { - statements.push(tstl.createGotoStatement(`${switchName}_case_default`)); - } else { - statements.push(tstl.createGotoStatement(`${switchName}_end`)); - } - - for (let i = 0; i < statement.caseBlock.clauses.length; i++) { - const clause = statement.caseBlock.clauses[i]; - const label = ts.isCaseClause(clause) - ? tstl.createLabelStatement(`${switchName}_case_${i}`) - : tstl.createLabelStatement(`${switchName}_case_default`); - - const body = tstl.createDoStatement(this.transformStatements(clause.statements)); - statements.push(label, body); - } - - statements.push(tstl.createLabelStatement(`${switchName}_end`)); - - statements = this.performHoisting(statements); - this.popScope(); - - return statements; - } - - public transformBreakStatement(breakStatement: ts.BreakStatement): StatementVisitResult { - const breakableScope = this.findScope(ScopeType.Loop | ScopeType.Switch); - if (breakableScope.type === ScopeType.Switch) { - return tstl.createGotoStatement(`____TS_switch${breakableScope.id}_end`); - } else { - return tstl.createBreakStatement(breakStatement); - } - } - - public transformTryStatement(statement: ts.TryStatement): StatementVisitResult { - const pCall = tstl.createIdentifier("pcall"); - const tryBlock = this.transformBlock(statement.tryBlock); - const tryCall = tstl.createCallExpression(pCall, [tstl.createFunctionExpression(tryBlock)]); - - const result: tstl.Statement[] = []; - - if (statement.catchClause) { - const tryResult = tstl.createIdentifier("____TS_try"); - - const returnVariables = statement.catchClause && statement.catchClause.variableDeclaration - ? [tryResult, this.transformIdentifier(statement.catchClause.variableDeclaration.name as ts.Identifier)] - : [tryResult]; - - const catchAssignment = tstl.createVariableDeclarationStatement(returnVariables, tryCall); - - result.push(catchAssignment); - - const notTryResult = tstl.createUnaryExpression(tryResult, tstl.SyntaxKind.NotOperator); - result.push(tstl.createIfStatement(notTryResult, this.transformBlock(statement.catchClause.block))); - - } else { - result.push(tstl.createExpressionStatement(tryCall)); - } - - if (statement.finallyBlock) { - result.push(tstl.createDoStatement(this.transformBlock(statement.finallyBlock).statements)); - } - - return tstl.createDoStatement( - result, - statement - ); - } - - public transformThrowStatement(statement: ts.ThrowStatement): StatementVisitResult { - const type = this.checker.getTypeAtLocation(statement.expression); - if (tsHelper.isStringType(type)) { - const error = tstl.createIdentifier("error"); - return tstl.createExpressionStatement( - tstl.createCallExpression(error, [this.transformExpression(statement.expression)]), - statement - ); - } else { - throw TSTLErrors.InvalidThrowExpression(statement.expression); - } - } - - public transformContinueStatement(statement: ts.ContinueStatement): StatementVisitResult { - if (this.options.luaTarget === LuaTarget.Lua51) { - throw TSTLErrors.UnsupportedForTarget("Continue statement", this.options.luaTarget, statement); - } - - return tstl.createGotoStatement( - `__continue${this.findScope(ScopeType.Loop).id}`, - statement - ); - } - - public transformEmptyStatement(arg0: ts.EmptyStatement): StatementVisitResult { - return undefined; - } - - // Expressions - public transformExpression(expression: ts.Expression): ExpressionVisitResult { - switch (expression.kind) { - case ts.SyntaxKind.BinaryExpression: - return this.transformBinaryExpression(expression as ts.BinaryExpression); - case ts.SyntaxKind.ConditionalExpression: - return this.transformConditionalExpression(expression as ts.ConditionalExpression); - case ts.SyntaxKind.CallExpression: - return this.transformCallExpression(expression as ts.CallExpression); - case ts.SyntaxKind.PropertyAccessExpression: - return this.transformPropertyAccessExpression(expression as ts.PropertyAccessExpression); - case ts.SyntaxKind.ElementAccessExpression: - return this.transformElementAccessExpression(expression as ts.ElementAccessExpression); - case ts.SyntaxKind.Identifier: - return this.transformIdentifierExpression(expression as ts.Identifier); - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return this.transformStringLiteral(expression as ts.StringLiteral); - case ts.SyntaxKind.TemplateExpression: - return this.transformTemplateExpression(expression as ts.TemplateExpression); - case ts.SyntaxKind.NumericLiteral: - return this.transformNumericLiteral(expression as ts.NumericLiteral); - case ts.SyntaxKind.TrueKeyword: - return this.transformTrueKeyword(expression as ts.BooleanLiteral); - case ts.SyntaxKind.FalseKeyword: - return this.transformFalseKeyword(expression as ts.BooleanLiteral); - case ts.SyntaxKind.NullKeyword: - case ts.SyntaxKind.UndefinedKeyword: - return this.transformNullOrUndefinedKeyword(expression); - case ts.SyntaxKind.ThisKeyword: - return this.transformThisKeyword(expression as ts.ThisExpression); - case ts.SyntaxKind.PostfixUnaryExpression: - return this.transformPostfixUnaryExpression(expression as ts.PostfixUnaryExpression); - case ts.SyntaxKind.PrefixUnaryExpression: - return this.transformPrefixUnaryExpression(expression as ts.PrefixUnaryExpression); - case ts.SyntaxKind.ArrayLiteralExpression: - return this.transformArrayLiteral(expression as ts.ArrayLiteralExpression); - case ts.SyntaxKind.ObjectLiteralExpression: - return this.transformObjectLiteral(expression as ts.ObjectLiteralExpression); - case ts.SyntaxKind.DeleteExpression: - return this.transformDeleteExpression(expression as ts.DeleteExpression); - case ts.SyntaxKind.FunctionExpression: - return this.transformFunctionExpression(expression as ts.ArrowFunction, this.createSelfIdentifier()); - case ts.SyntaxKind.ArrowFunction: - return this.transformFunctionExpression(expression as ts.ArrowFunction, tstl.createIdentifier("____")); - case ts.SyntaxKind.NewExpression: - return this.transformNewExpression(expression as ts.NewExpression); - case ts.SyntaxKind.ParenthesizedExpression: - return this.transformParenthesizedExpression(expression as ts.ParenthesizedExpression); - case ts.SyntaxKind.SuperKeyword: - return this.transformSuperKeyword(expression as ts.SuperExpression); - case ts.SyntaxKind.TypeAssertionExpression: - case ts.SyntaxKind.AsExpression: - return this.transformAssertionExpression(expression as ts.AssertionExpression); - case ts.SyntaxKind.TypeOfExpression: - return this.transformTypeOfExpression(expression as ts.TypeOfExpression); - case ts.SyntaxKind.SpreadElement: - return this.transformSpreadElement(expression as ts.SpreadElement); - case ts.SyntaxKind.NonNullExpression: - return this.transformExpression((expression as ts.NonNullExpression).expression); - case ts.SyntaxKind.YieldExpression: - return this.transformYield(expression as ts.YieldExpression); - case ts.SyntaxKind.EmptyStatement: - return undefined; - case ts.SyntaxKind.NotEmittedStatement: - return undefined; - case ts.SyntaxKind.ClassExpression: - const className = tstl.createIdentifier("____"); - const classDeclaration = this.transformClassDeclaration(expression as ts.ClassExpression, className); - return this.createImmediatelyInvokedFunctionExpression(classDeclaration, className, expression); - case ts.SyntaxKind.PartiallyEmittedExpression: - return this.transformExpression((expression as ts.PartiallyEmittedExpression).expression); - default: - throw TSTLErrors.UnsupportedKind("expression", expression.kind, expression); - } - } - - public transformBinaryOperation( - node: ts.Node, - left: tstl.Expression, - right: tstl.Expression, - operator: tstl.BinaryOperator - ): tstl.Expression - { - switch (operator) { - case tstl.SyntaxKind.BitwiseAndOperator: - case tstl.SyntaxKind.BitwiseOrOperator: - case tstl.SyntaxKind.BitwiseExclusiveOrOperator: - case tstl.SyntaxKind.BitwiseLeftShiftOperator: - case tstl.SyntaxKind.BitwiseRightShiftOperator: - case tstl.SyntaxKind.BitwiseArithmeticRightShift: - return this.transformBinaryBitOperation(node, left, right, operator); - - default: - return tstl.createBinaryExpression(left, right, operator, node); - } - } - - public transformBinaryExpression(expression: ts.BinaryExpression): tstl.Expression { - // Check if this is an assignment token, then handle accordingly - - const [isCompound, replacementOperator] = tsHelper.isBinaryAssignmentToken(expression.operatorToken.kind); - if (isCompound) { - return this.transformCompoundAssignmentExpression( - expression, - expression.left, - expression.right, - replacementOperator, - false - ); - } - - const lhs = this.transformExpression(expression.left); - const rhs = this.transformExpression(expression.right); - - // Transpile operators - switch (expression.operatorToken.kind) { - case ts.SyntaxKind.AmpersandToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.BitwiseAndOperator); - case ts.SyntaxKind.BarToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.BitwiseOrOperator); - case ts.SyntaxKind.CaretToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.BitwiseExclusiveOrOperator); - case ts.SyntaxKind.LessThanLessThanToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.BitwiseLeftShiftOperator); - case ts.SyntaxKind.GreaterThanGreaterThanToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.BitwiseRightShiftOperator); - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.BitwiseArithmeticRightShift); - case ts.SyntaxKind.AmpersandAmpersandToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.AndOperator); - case ts.SyntaxKind.BarBarToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.OrOperator); - case ts.SyntaxKind.PlusToken: - // Replace string + with .. - const typeLeft = this.checker.getTypeAtLocation(expression.left); - const typeRight = this.checker.getTypeAtLocation(expression.right); - if (tsHelper.isStringType(typeLeft) || tsHelper.isStringType(typeRight)) { - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.ConcatOperator); - } - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.AdditionOperator); - case ts.SyntaxKind.MinusToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.SubractionOperator); - case ts.SyntaxKind.AsteriskToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.MultiplicationOperator); - case ts.SyntaxKind.AsteriskAsteriskToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.PowerOperator); - case ts.SyntaxKind.SlashToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.DivisionOperator); - case ts.SyntaxKind.PercentToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.ModuloOperator); - case ts.SyntaxKind.GreaterThanToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.GreaterThanOperator); - case ts.SyntaxKind.GreaterThanEqualsToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.GreaterEqualOperator); - case ts.SyntaxKind.LessThanToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.LessThanOperator); - case ts.SyntaxKind.LessThanEqualsToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.LessEqualOperator); - case ts.SyntaxKind.EqualsToken: - return this.transformAssignmentExpression(expression); - case ts.SyntaxKind.EqualsEqualsToken: - case ts.SyntaxKind.EqualsEqualsEqualsToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.EqualityOperator); - case ts.SyntaxKind.ExclamationEqualsToken: - case ts.SyntaxKind.ExclamationEqualsEqualsToken: - return this.transformBinaryOperation(expression, lhs, rhs, tstl.SyntaxKind.InequalityOperator); - case ts.SyntaxKind.InKeyword: - const indexExpression = tstl.createTableIndexExpression(rhs, lhs); - return tstl.createBinaryExpression( - indexExpression, - tstl.createNilLiteral(), - tstl.SyntaxKind.InequalityOperator, - expression - ); - - case ts.SyntaxKind.InstanceOfKeyword: - const decorators = tsHelper.getCustomDecorators( - this.checker.getTypeAtLocation(expression.right), - this.checker - ); - if (decorators.has(DecoratorKind.Extension) || decorators.has(DecoratorKind.MetaExtension)) { - // Cannot use instanceof on extension classes - throw TSTLErrors.InvalidInstanceOfExtension(expression); - } - return this.transformLuaLibFunction(LuaLibFeature.InstanceOf, expression, lhs, rhs); - - case ts.SyntaxKind.CommaToken: - return this.createImmediatelyInvokedFunctionExpression( - [this.transformExpressionStatement(expression.left)], - rhs, - expression - ); - - default: - throw TSTLErrors.UnsupportedKind("binary operator", expression.operatorToken.kind, expression); - } - } - - public transformAssignment(lhs: ts.Expression, right: tstl.Expression): tstl.Statement { - return tstl.createAssignmentStatement( - this.transformExpression(lhs) as tstl.IdentifierOrTableIndexExpression, - right, - lhs.parent - ); - } - - public transformAssignmentStatement(expression: ts.BinaryExpression): tstl.Statement { - // Validate assignment - const rightType = this.checker.getTypeAtLocation(expression.right); - const leftType = this.checker.getTypeAtLocation(expression.left); - this.validateFunctionAssignment(expression.right, rightType, leftType); - - if (ts.isArrayLiteralExpression(expression.left)) { - // Destructuring assignment - const left = expression.left.elements.map(e => this.transformExpression(e)); - let right: tstl.Expression[]; - if (ts.isArrayLiteralExpression(expression.right)) { - right = expression.right.elements.map(e => this.transformExpression(e)); - } else if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { - right = [this.transformExpression(expression.right)]; - } else { - right = [this.createUnpackCall(this.transformExpression(expression.right), expression.right)]; - } - return tstl.createAssignmentStatement( - left as tstl.IdentifierOrTableIndexExpression[], - right, - expression - ); - } else { - // Simple assignment - return this.transformAssignment(expression.left, this.transformExpression(expression.right)); - } - } - - public transformAssignmentExpression(expression: ts.BinaryExpression) - : tstl.CallExpression | tstl.MethodCallExpression - { - // Validate assignment - const rightType = this.checker.getTypeAtLocation(expression.right); - const leftType = this.checker.getTypeAtLocation(expression.left); - this.validateFunctionAssignment(expression.right, rightType, leftType); - - if (ts.isArrayLiteralExpression(expression.left)) { - // Destructuring assignment - // (function() local ${tmps} = ${right}; ${left} = ${tmps}; return {${tmps}} end)() - const left = expression.left.elements.map(e => this.transformExpression(e)); - let right: tstl.Expression[]; - if (ts.isArrayLiteralExpression(expression.right)) { - right = expression.right.elements.map(e => this.transformExpression(e)); - } else if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { - right = [this.transformExpression(expression.right)]; - } else { - right = [this.createUnpackCall(this.transformExpression(expression.right), expression.right)]; - } - const tmps = expression.left.elements.map((_, i) => tstl.createIdentifier(`____TS_tmp${i}`)); - const statements: tstl.Statement[] = [ - tstl.createVariableDeclarationStatement(tmps, right), - tstl.createAssignmentStatement(left as tstl.IdentifierOrTableIndexExpression[], tmps), - ]; - return this.createImmediatelyInvokedFunctionExpression( - statements, - tstl.createTableExpression(tmps.map(t => tstl.createTableFieldExpression(t))), - expression - ); - } - - if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) { - // Left is property/element access: cache result while maintaining order of evaluation - // (function(o, i, v) o[i] = v; return v end)(${objExpression}, ${indexExpression}, ${right}) - const objParameter = tstl.createIdentifier("o"); - const indexParameter = tstl.createIdentifier("i"); - const valueParameter = tstl.createIdentifier("v"); - const indexStatement = tstl.createTableIndexExpression(objParameter, indexParameter); - const statements: tstl.Statement[] = [ - tstl.createAssignmentStatement(indexStatement, valueParameter), - tstl.createReturnStatement([valueParameter]), - ]; - const iife = tstl.createFunctionExpression( - tstl.createBlock(statements), - [objParameter, indexParameter, valueParameter] - ); - const objExpression = this.transformExpression(expression.left.expression); - let indexExpression: tstl.Expression; - if (ts.isPropertyAccessExpression(expression.left)) { - // Property access - indexExpression = tstl.createStringLiteral(expression.left.name.text); - } else { - // Element access - indexExpression = this.transformExpression(expression.left.argumentExpression); - const argType = this.checker.getTypeAtLocation(expression.left.expression); - if (tsHelper.isArrayType(argType, this.checker)) { - // Array access needs a +1 - indexExpression = this.expressionPlusOne(indexExpression); - } - } - const args = [objExpression, indexExpression, this.transformExpression(expression.right)]; - return tstl.createCallExpression(tstl.createParenthesizedExpression(iife), args); - - } else { - // Simple assignment - // (function() ${left} = ${right}; return ${left} end)() - const left = this.transformExpression(expression.left); - const right = this.transformExpression(expression.right); - return this.createImmediatelyInvokedFunctionExpression( - [this.transformAssignment(expression.left, right)], - left, - expression - ); - } - } - - public transformCompoundAssignmentExpression( - expression: ts.Expression, - lhs: ts.Expression, - rhs: ts.Expression, - replacementOperator: tstl.BinaryOperator, - isPostfix: boolean - ): tstl.CallExpression - { - if (replacementOperator === tstl.SyntaxKind.AdditionOperator) { - // Check is we need to use string concat operator - const typeLeft = this.checker.getTypeAtLocation(lhs); - const typeRight = this.checker.getTypeAtLocation(rhs); - if (tsHelper.isStringType(typeLeft) || tsHelper.isStringType(typeRight)) { - replacementOperator = tstl.SyntaxKind.ConcatOperator; - } - } - - const left = this.transformExpression(lhs) as tstl.IdentifierOrTableIndexExpression; - let right = this.transformExpression(rhs); - - const [hasEffects, objExpression, indexExpression] = tsHelper.isAccessExpressionWithEvaluationEffects( - lhs, - this.checker - ); - if (hasEffects) { - // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects - // local __TS_obj, __TS_index = ${objExpression}, ${indexExpression}; - const obj = tstl.createIdentifier("____TS_obj"); - const index = tstl.createIdentifier("____TS_index"); - const objAndIndexDeclaration = tstl.createVariableDeclarationStatement( - [obj, index], [this.transformExpression(objExpression), this.transformExpression(indexExpression)]); - const accessExpression = tstl.createTableIndexExpression(obj, index); - - const tmp = tstl.createIdentifier("____TS_tmp"); - right = tstl.createParenthesizedExpression(right); - let tmpDeclaration: tstl.VariableDeclarationStatement; - let assignStatement: tstl.AssignmentStatement; - if (isPostfix) { - // local ____TS_tmp = ____TS_obj[____TS_index]; - // ____TS_obj[____TS_index] = ____TS_tmp ${replacementOperator} ${right}; - tmpDeclaration = tstl.createVariableDeclarationStatement(tmp, accessExpression); - const operatorExpression = this.transformBinaryOperation(expression, tmp, right, replacementOperator); - assignStatement = tstl.createAssignmentStatement(accessExpression, operatorExpression); - } else { - // local ____TS_tmp = ____TS_obj[____TS_index] ${replacementOperator} ${right}; - // ____TS_obj[____TS_index] = ____TS_tmp; - const operatorExpression = this.transformBinaryOperation( - expression, - accessExpression, - right, - replacementOperator - ); - tmpDeclaration = tstl.createVariableDeclarationStatement(tmp, operatorExpression); - assignStatement = tstl.createAssignmentStatement(accessExpression, tmp); - } - // return ____TS_tmp - return this.createImmediatelyInvokedFunctionExpression( - [objAndIndexDeclaration, tmpDeclaration, assignStatement], - tmp, - lhs.parent - ); - - } else if (isPostfix) { - // Postfix expressions need to cache original value in temp - // local ____TS_tmp = ${left}; - // ${left} = ____TS_tmp ${replacementOperator} ${right}; - // return ____TS_tmp - const tmpIdentifier = tstl.createIdentifier("____TS_tmp"); - const tmpDeclaration = tstl.createVariableDeclarationStatement(tmpIdentifier, left); - const operatorExpression = this.transformBinaryOperation( - expression, - tmpIdentifier, - right, - replacementOperator - ); - const assignStatement = this.transformAssignment(lhs, operatorExpression); - return this.createImmediatelyInvokedFunctionExpression( - [tmpDeclaration, assignStatement], - tmpIdentifier, - lhs.parent - ); - - } else if (ts.isPropertyAccessExpression(lhs) || ts.isElementAccessExpression(lhs)) { - // Simple property/element access expressions need to cache in temp to avoid double-evaluation - // local ____TS_tmp = ${left} ${replacementOperator} ${right}; - // ${left} = ____TS_tmp; - // return ____TS_tmp - const tmpIdentifier = tstl.createIdentifier("____TS_tmp"); - const operatorExpression = this.transformBinaryOperation(lhs.parent, left, right, replacementOperator); - const tmpDeclaration = tstl.createVariableDeclarationStatement(tmpIdentifier, operatorExpression); - const assignStatement = this.transformAssignment(lhs, tmpIdentifier); - return this.createImmediatelyInvokedFunctionExpression( - [tmpDeclaration, assignStatement], - tmpIdentifier, - lhs.parent - ); - - } else { - // Simple expressions - // ${left} = ${right}; return ${right} - const operatorExpression = this.transformBinaryOperation(lhs.parent, left, right, replacementOperator); - const assignStatement = this.transformAssignment(lhs, operatorExpression); - return this.createImmediatelyInvokedFunctionExpression([assignStatement], left, lhs.parent); - } - } - - public transformCompoundAssignmentStatement( - node: ts.Node, - lhs: ts.Expression, - rhs: ts.Expression, - replacementOperator: tstl.BinaryOperator - ): tstl.Statement - { - if (replacementOperator === tstl.SyntaxKind.AdditionOperator) { - // Check is we need to use string concat operator - const typeLeft = this.checker.getTypeAtLocation(lhs); - const typeRight = this.checker.getTypeAtLocation(rhs); - if (tsHelper.isStringType(typeLeft) || tsHelper.isStringType(typeRight)) { - replacementOperator = tstl.SyntaxKind.ConcatOperator; - } - } - - const left = this.transformExpression(lhs) as tstl.IdentifierOrTableIndexExpression; - const right = this.transformExpression(rhs); - - const [hasEffects, objExpression, indexExpression] = tsHelper.isAccessExpressionWithEvaluationEffects( - lhs, - this.checker - ); - if (hasEffects) { - // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects - // local __TS_obj, __TS_index = ${objExpression}, ${indexExpression}; - // ____TS_obj[____TS_index] = ____TS_obj[____TS_index] ${replacementOperator} ${right}; - const obj = tstl.createIdentifier("____TS_obj"); - const index = tstl.createIdentifier("____TS_index"); - const objAndIndexDeclaration = tstl.createVariableDeclarationStatement( - [obj, index], [this.transformExpression(objExpression), this.transformExpression(indexExpression)]); - const accessExpression = tstl.createTableIndexExpression(obj, index); - const operatorExpression = this.transformBinaryOperation( - node, - accessExpression, - tstl.createParenthesizedExpression(right), - replacementOperator - ); - const assignStatement = tstl.createAssignmentStatement(accessExpression, operatorExpression); - return tstl.createDoStatement([objAndIndexDeclaration, assignStatement]); - - } else { - // Simple statements - // ${left} = ${left} ${replacementOperator} ${right} - const operatorExpression = this.transformBinaryOperation(node, left, right, replacementOperator); - return this.transformAssignment(lhs, operatorExpression); - } - } - - public transformUnaryBitLibOperation( - node: ts.Node, - expression: tstl.Expression, - operator: tstl.UnaryBitwiseOperator, - lib: string - ): ExpressionVisitResult - { - let bitFunction: string; - switch (operator) { - case tstl.SyntaxKind.BitwiseNotOperator: - bitFunction = "bnot"; - break; - default: - throw TSTLErrors.UnsupportedKind("unary bitwise operator", operator, node); - } - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier(lib), tstl.createStringLiteral(bitFunction)), - [expression], - node - ); - } - - public transformUnaryBitOperation( - node: ts.Node, - expression: tstl.Expression, - operator: tstl.UnaryBitwiseOperator - ): ExpressionVisitResult - { - switch (this.options.luaTarget) { - case LuaTarget.Lua51: - throw TSTLErrors.UnsupportedForTarget("Bitwise operations", this.options.luaTarget, node); - - case LuaTarget.Lua52: - return this.transformUnaryBitLibOperation(node, expression, operator, "bit32"); - - case LuaTarget.LuaJIT: - return this.transformUnaryBitLibOperation(node, expression, operator, "bit"); - - default: - return tstl.createUnaryExpression(expression, operator, node); - } - } - - public transformBinaryBitLibOperation( - node: ts.Node, - left: tstl.Expression, - right: tstl.Expression, - operator: tstl.BinaryBitwiseOperator, - lib: string - ): ExpressionVisitResult - { - let bitFunction: string; - switch (operator) { - case tstl.SyntaxKind.BitwiseAndOperator: - bitFunction = "band"; - break; - case tstl.SyntaxKind.BitwiseOrOperator: - bitFunction = "bor"; - break; - case tstl.SyntaxKind.BitwiseExclusiveOrOperator: - bitFunction = "bxor"; - break; - case tstl.SyntaxKind.BitwiseLeftShiftOperator: - bitFunction = "lshift"; - break; - case tstl.SyntaxKind.BitwiseRightShiftOperator: - bitFunction = "rshift"; - break; - case tstl.SyntaxKind.BitwiseArithmeticRightShift: - bitFunction = "arshift"; - break; - default: - throw TSTLErrors.UnsupportedKind("binary bitwise operator", operator, node); - } - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier(lib), tstl.createStringLiteral(bitFunction)), - [left, right], - node - ); - } - - public transformBinaryBitOperation( - node: ts.Node, - left: tstl.Expression, - right: tstl.Expression, - operator: tstl.BinaryBitwiseOperator - ): ExpressionVisitResult - { - switch (this.options.luaTarget) { - case LuaTarget.Lua51: - throw TSTLErrors.UnsupportedForTarget("Bitwise operations", this.options.luaTarget, node); - - case LuaTarget.Lua52: - return this.transformBinaryBitLibOperation(node, left, right, operator, "bit32"); - - case LuaTarget.LuaJIT: - return this.transformBinaryBitLibOperation(node, left, right, operator, "bit"); - - default: - if (operator === tstl.SyntaxKind.BitwiseArithmeticRightShift) { - throw TSTLErrors.UnsupportedForTarget("Bitwise >>> operator", this.options.luaTarget, node); - } - return tstl.createBinaryExpression(left, right, operator, node); - } - } - - public transformProtectedConditionalExpression(expression: ts.ConditionalExpression): tstl.CallExpression { - const condition = this.transformExpression(expression.condition); - const val1 = this.transformExpression(expression.whenTrue); - const val2 = this.transformExpression(expression.whenFalse); - - const val1Function = this.wrapInFunctionCall(val1); - const val2Function = this.wrapInFunctionCall(val2); - - // ((condition and (() => v1)) or (() => v2))() - const conditionAnd = tstl.createBinaryExpression(condition, val1Function, tstl.SyntaxKind.AndOperator); - const orExpression = tstl.createBinaryExpression(conditionAnd, val2Function, tstl.SyntaxKind.OrOperator); - return tstl.createCallExpression(orExpression, [], expression); - } - - public transformConditionalExpression(expression: ts.ConditionalExpression): tstl.Expression { - const isStrict = this.options.strict || this.options.strictNullChecks; - if (tsHelper.isFalsible(this.checker.getTypeAtLocation(expression.whenTrue), isStrict)) { - return this.transformProtectedConditionalExpression(expression); - } - const condition = this.transformExpression(expression.condition); - const val1 = this.transformExpression(expression.whenTrue); - const val2 = this.transformExpression(expression.whenFalse); - - // (condition and v1) or v2 - const conditionAnd = tstl.createBinaryExpression(condition, val1, tstl.SyntaxKind.AndOperator); - return tstl.createBinaryExpression( - conditionAnd, - val2, - tstl.SyntaxKind.OrOperator, - expression - ); - } - - public transformPostfixUnaryExpression(expression: ts.PostfixUnaryExpression): tstl.Expression { - switch (expression.operator) { - case ts.SyntaxKind.PlusPlusToken: - return this.transformCompoundAssignmentExpression( - expression, - expression.operand, - ts.createLiteral(1), - tstl.SyntaxKind.AdditionOperator, - true - ); - - case ts.SyntaxKind.MinusMinusToken: - return this.transformCompoundAssignmentExpression( - expression, - expression.operand, - ts.createLiteral(1), - tstl.SyntaxKind.SubractionOperator, - true - ); - - default: - throw TSTLErrors.UnsupportedKind("unary postfix operator", expression.operator, expression); - } - } - - public transformPrefixUnaryExpression(expression: ts.PrefixUnaryExpression): tstl.Expression { - switch (expression.operator) { - case ts.SyntaxKind.PlusPlusToken: - return this.transformCompoundAssignmentExpression( - expression, - expression.operand, - ts.createLiteral(1), - tstl.SyntaxKind.AdditionOperator, - false - ); - - case ts.SyntaxKind.MinusMinusToken: - return this.transformCompoundAssignmentExpression( - expression, - expression.operand, - ts.createLiteral(1), - tstl.SyntaxKind.SubractionOperator, - false - ); - - case ts.SyntaxKind.PlusToken: - return this.transformExpression(expression.operand); - - case ts.SyntaxKind.MinusToken: - return tstl.createUnaryExpression( - this.transformExpression(expression.operand), - tstl.SyntaxKind.NegationOperator - ); - - case ts.SyntaxKind.ExclamationToken: - return tstl.createUnaryExpression( - this.transformExpression(expression.operand), - tstl.SyntaxKind.NotOperator - ); - - case ts.SyntaxKind.TildeToken: - return this.transformUnaryBitOperation( - expression, - this.transformExpression(expression.operand), - tstl.SyntaxKind.BitwiseNotOperator - ); - - default: - throw TSTLErrors.UnsupportedKind("unary prefix operator", expression.operator, expression); - } - } - - public transformArrayLiteral(node: ts.ArrayLiteralExpression): tstl.TableExpression { - const values: tstl.TableFieldExpression[] = []; - - node.elements.forEach(child => { - values.push(tstl.createTableFieldExpression(this.transformExpression(child), undefined, child)); - }); - - return tstl.createTableExpression(values, node); - } - - public transformObjectLiteral(node: ts.ObjectLiteralExpression): tstl.TableExpression { - const properties: tstl.TableFieldExpression[] = []; - // Add all property assignments - node.properties.forEach(element => { - const name = this.transformPropertyName(element.name); - if (ts.isPropertyAssignment(element)) { - const expression = this.transformExpression(element.initializer); - properties.push(tstl.createTableFieldExpression(expression, name, element)); - } else if (ts.isShorthandPropertyAssignment(element)) { - const identifier = this.transformIdentifier(element.name); - properties.push(tstl.createTableFieldExpression(identifier, name, element)); - } else if (ts.isMethodDeclaration(element)) { - const expression = this.transformFunctionExpression(element, this.createSelfIdentifier()); - properties.push(tstl.createTableFieldExpression(expression, name, element)); - } else { - throw TSTLErrors.UnsupportedKind("object literal element", element.kind, node); - } - }); - - return tstl.createTableExpression(properties, node); - } - - public transformDeleteExpression(expression: ts.DeleteExpression): tstl.CallExpression { - const lhs = this.transformExpression(expression.expression) as tstl.IdentifierOrTableIndexExpression; - const assignment = tstl.createAssignmentStatement( - lhs, - tstl.createNilLiteral(), - expression - ); - - return this.createImmediatelyInvokedFunctionExpression( - [assignment], - [tstl.createBooleanLiteral(true)], - expression - ); - } - - public transformFunctionExpression( - node: ts.FunctionLikeDeclaration, - context: tstl.Identifier | undefined - ): ExpressionVisitResult - { - const type = this.checker.getTypeAtLocation(node); - const hasContext = tsHelper.getFunctionContextType(type, this.checker) !== ContextType.Void; - // Build parameter string - const [paramNames, dotsLiteral, spreadIdentifier] = this.transformParameters( - node.parameters, - hasContext ? context : undefined - ); - - const body = ts.isBlock(node.body) ? node.body : ts.createBlock([ts.createReturn(node.body)]); - const [transformedBody] = this.transformFunctionBody(node.parameters, body, spreadIdentifier); - - return tstl.createFunctionExpression( - tstl.createBlock(transformedBody), - paramNames, - dotsLiteral, - spreadIdentifier, - node - ); - } - - public transformNewExpression(node: ts.NewExpression): tstl.CallExpression { - const name = this.transformExpression(node.expression); - const sig = this.checker.getResolvedSignature(node); - const params = node.arguments - ? this.transformArguments(node.arguments, sig) - : [tstl.createBooleanLiteral(true)]; - - const type = this.checker.getTypeAtLocation(node); - const classDecorators = tsHelper.getCustomDecorators(type, this.checker); - - this.checkForLuaLibType(type); - - if (classDecorators.has(DecoratorKind.Extension) || classDecorators.has(DecoratorKind.MetaExtension)) { - throw TSTLErrors.InvalidNewExpressionOnExtension(node); - } - - if (classDecorators.has(DecoratorKind.CustomConstructor)) { - const customDecorator = classDecorators.get(DecoratorKind.CustomConstructor); - if (!customDecorator.args[0]) { - throw TSTLErrors.InvalidDecoratorArgumentNumber("!CustomConstructor", 0, 1, node); - } - return tstl.createCallExpression( - tstl.createIdentifier(customDecorator.args[0]), - this.transformArguments(node.arguments), - node - ); - } - - return tstl.createCallExpression( - tstl.createTableIndexExpression(name, tstl.createStringLiteral("new")), - params, - node - ); - } - - public transformParenthesizedExpression(expression: ts.ParenthesizedExpression): tstl.Expression { - return tstl.createParenthesizedExpression( - this.transformExpression(expression.expression), - expression - ); - } - - public transformSuperKeyword(expression: ts.SuperExpression): tstl.Expression { - const classDeclaration = this.classStack[this.classStack.length - 1]; - const extendsExpression = tsHelper.getExtendedTypeNode(classDeclaration, this.checker).expression; - let baseClassName: tstl.IdentifierOrTableIndexExpression; - if (ts.isIdentifier(extendsExpression)) { - // Use "baseClassName" if base is a simple identifier - baseClassName = this.addExportToIdentifier(this.transformIdentifier(extendsExpression)); - } else { - // Use "className.____super" if the base is not a simple identifier - baseClassName = tstl.createTableIndexExpression( - this.addExportToIdentifier(this.transformIdentifier(classDeclaration.name)), - tstl.createStringLiteral("____super"), - expression - ); - } - return tstl.createTableIndexExpression(baseClassName, tstl.createStringLiteral("prototype")); - } - - public transformCallExpression(node: ts.CallExpression): tstl.Expression { - // Check for calls on primitives to override - let parameters: tstl.Expression[] = []; - - const isLuaIterator = tsHelper.isLuaIteratorCall(node, this.checker); - const isTupleReturn = tsHelper.isTupleReturnCall(node, this.checker); - const isTupleReturnForward = - node.parent && ts.isReturnStatement(node.parent) && tsHelper.isInTupleReturnFunction(node, this.checker); - const isInDestructingAssignment = tsHelper.isInDestructingAssignment(node); - const returnValueIsUsed = node.parent && !ts.isExpressionStatement(node.parent); - const wrapResult = isTupleReturn && !isTupleReturnForward&& !isInDestructingAssignment - && returnValueIsUsed && !isLuaIterator; - - if (ts.isPropertyAccessExpression(node.expression)) { - const result = this.transformPropertyCall(node); - return wrapResult ? this.wrapInTable(result) : result; - } - - if (ts.isElementAccessExpression(node.expression)) { - const result = this.transformElementCall(node); - return wrapResult ? this.wrapInTable(result) : result; - } - - const signature = this.checker.getResolvedSignature(node); - - // Handle super calls properly - if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { - parameters = this.transformArguments(node.arguments, signature, ts.createThis()); - - return tstl.createCallExpression( - tstl.createTableIndexExpression( - this.transformSuperKeyword(ts.createSuper()), - tstl.createStringLiteral("____constructor") - ), - parameters - ); - } - - const callPath = this.transformExpression(node.expression); - const signatureDeclaration = signature.getDeclaration(); - if (signatureDeclaration - && !ts.isPropertyAccessExpression(node.expression) - && tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) === ContextType.NonVoid - && !ts.isElementAccessExpression(node.expression)) - { - const context = this.isStrict ? ts.createNull() : ts.createIdentifier("_G"); - parameters = this.transformArguments(node.arguments, signature, context); - } else { - parameters = this.transformArguments(node.arguments, signature); - } - - const expressionType = this.checker.getTypeAtLocation(node.expression); - if (expressionType.symbol && expressionType.symbol.escapedName === "SymbolConstructor") { - return this.transformLuaLibFunction(LuaLibFeature.Symbol, node, ...parameters); - } - - const callExpression = tstl.createCallExpression(callPath, parameters); - return wrapResult ? this.wrapInTable(callExpression) : callExpression; - } - - public transformPropertyCall(node: ts.CallExpression): tstl.Expression { - let parameters: tstl.Expression[] = []; - - // Check if call is actually on a property access expression - if (!ts.isPropertyAccessExpression(node.expression)) { - throw TSTLErrors.InvalidPropertyCall(node); - } - - // If the function being called is of type owner.func, get the type of owner - const ownerType = this.checker.getTypeAtLocation(node.expression.expression); - - if (ownerType.symbol && ownerType.symbol.escapedName === "Math") { - return tstl.createCallExpression( - this.transformMathExpression(node.expression.name), - this.transformArguments(node.arguments), - node - ); - } - - if (ownerType.symbol && ownerType.symbol.escapedName === "StringConstructor") { - return tstl.createCallExpression( - this.transformStringExpression(node.expression.name), - this.transformArguments(node.arguments), - node - ); - } - - if (ownerType.symbol && ownerType.symbol.escapedName === "ObjectConstructor") { - return this.transformObjectCallExpression(node); - } - - if (ownerType.symbol && ownerType.symbol.escapedName === "SymbolConstructor") { - return this.transformSymbolCallExpression(node); - } - - switch (ownerType.flags) { - case ts.TypeFlags.String: - case ts.TypeFlags.StringLiteral: - return this.transformStringCallExpression(node); - } - - // if ownerType is a array, use only supported functions - if (tsHelper.isExplicitArrayType(ownerType, this.checker)) { - return this.transformArrayCallExpression(node); - } - - // if ownerType inherits from an array, use array calls where appropriate - if (tsHelper.isArrayType(ownerType, this.checker) && - tsHelper.isDefaultArrayCallMethodName(node.expression.name.escapedText as string)) { - return this.transformArrayCallExpression(node); - } - - if (tsHelper.isFunctionType(ownerType, this.checker)) { - return this.transformFunctionCallExpression(node); - } - - const signature = this.checker.getResolvedSignature(node); - - // Get the type of the function - if (node.expression.expression.kind === ts.SyntaxKind.SuperKeyword) { - // Super calls take the format of super.call(self,...) - parameters = this.transformArguments(node.arguments, signature, ts.createThis()); - return tstl.createCallExpression(this.transformExpression(node.expression), parameters); - } else { - // Replace last . with : here - const name = node.expression.name.escapedText; - if (name === "toString") { - const toStringIdentifier = tstl.createIdentifier("tostring"); - return tstl.createCallExpression( - toStringIdentifier, [this.transformExpression(node.expression.expression)], node); - } else if (name === "hasOwnProperty") { - const expr = this.transformExpression(node.expression.expression); - parameters = this.transformArguments(node.arguments, signature); - const rawGetIdentifier = tstl.createIdentifier("rawget"); - const rawGetCall = tstl.createCallExpression(rawGetIdentifier, [expr, ...parameters]); - return tstl.createBinaryExpression( - rawGetCall, tstl.createNilLiteral(), tstl.SyntaxKind.InequalityOperator, node); - } else { - const parameters = this.transformArguments(node.arguments, signature); - const table = this.transformExpression(node.expression.expression); - const signatureDeclaration = signature.getDeclaration(); - if (!signatureDeclaration - || tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== ContextType.Void) - { - // table:name() - return tstl.createMethodCallExpression( - table, - tstl.createIdentifier(name), - parameters, - node - ); - } else { - // table.name() - const callPath = tstl.createTableIndexExpression(table, tstl.createStringLiteral(name)); - return tstl.createCallExpression(callPath, parameters, node); - } - } - } - } - - public transformElementCall(node: ts.CallExpression): tstl.CallExpression { - if (!ts.isElementAccessExpression(node.expression)) { - throw TSTLErrors.InvalidElementCall(node); - } - - const signature = this.checker.getResolvedSignature(node); - let parameters = this.transformArguments(node.arguments, signature); - - const signatureDeclaration = signature.getDeclaration(); - if (!signatureDeclaration - || tsHelper.getDeclarationContextType(signatureDeclaration, this.checker) !== ContextType.Void) { - // Pass left-side as context - - const context = this.transformExpression(node.expression.expression); - if (tsHelper.isExpressionWithEvaluationEffect(node.expression.expression)) { - // Inject context parameter - if (node.arguments.length > 0) { - parameters.unshift(tstl.createIdentifier("____TS_self")); - } else { - parameters = [tstl.createIdentifier("____TS_self")]; - } - - // Cache left-side if it has effects - //(function() local ____TS_self = context; return ____TS_self[argument](parameters); end)() - const argument = this.transformExpression(node.expression.argumentExpression); - const selfIdentifier = tstl.createIdentifier("____TS_self"); - const selfAssignment = tstl.createVariableDeclarationStatement(selfIdentifier, context); - const index = tstl.createTableIndexExpression(selfIdentifier, argument); - const callExpression = tstl.createCallExpression(index, parameters); - return this.createImmediatelyInvokedFunctionExpression([selfAssignment], callExpression, node); - } else { - return tstl.createCallExpression(this.transformExpression(node.expression), [context, ...parameters]); - } - } else { - // No context - return tstl.createCallExpression(this.transformExpression(node.expression), parameters); - } - } - - public transformArguments( - params: ts.NodeArray, - sig?: ts.Signature, context?: T - ): tstl.Expression[] - { - const parameters: tstl.Expression[] = []; - - // Add context as first param if present - if (context) { - parameters.push(this.transformExpression(context)); - } - - if (sig && sig.parameters.length >= params.length) { - for (let i = 0; i < params.length; ++i) { - const param = params[i]; - const paramType = this.checker.getTypeAtLocation(param); - const sigType = this.checker.getTypeAtLocation(sig.parameters[i].valueDeclaration); - this.validateFunctionAssignment(param, paramType, sigType, sig.parameters[i].name); - parameters.push(this.transformExpression(param)); - } - } else { - params.forEach(param => { - parameters.push(this.transformExpression(param)); - }); - } - - return parameters; - } - - public transformPropertyAccessExpression(node: ts.PropertyAccessExpression): tstl.Expression { - const property = node.name.text; - - // Check for primitive types to override - const type = this.checker.getTypeAtLocation(node.expression); - if (tsHelper.isStringType(type)) { - return this.transformStringProperty(node); - - } else if (tsHelper.isArrayType(type, this.checker)) { - const arrayPropertyAccess = this.transformArrayProperty(node); - if (arrayPropertyAccess) { - return arrayPropertyAccess; - } - - } else if (type.symbol && (type.symbol.flags & ts.SymbolFlags.ConstEnum)) { - return this.transformConstEnumValue(type, property, node); - } - - this.checkForLuaLibType(type); - - const decorators = tsHelper.getCustomDecorators(type, this.checker); - // Do not output path for member only enums - if (decorators.has(DecoratorKind.CompileMembersOnly)) { - return tstl.createIdentifier(property, node); - } - - // Catch math expressions - if (ts.isIdentifier(node.expression)) { - if (node.expression.escapedText === "Math") { - return this.transformMathExpression(node.name); - } else if (node.expression.escapedText === "Symbol") { - // Pull in Symbol lib - this.importLuaLibFeature(LuaLibFeature.Symbol); - } - } - - const callPath = this.transformExpression(node.expression); - return tstl.createTableIndexExpression(callPath, tstl.createStringLiteral(property), node); - } - - // Transpile a Math._ property - public transformMathExpression(identifier: ts.Identifier): tstl.TableIndexExpression { - const translation = { - PI: "pi", - abs: "abs", - acos: "acos", - asin: "asin", - atan: "atan", - ceil: "ceil", - cos: "cos", - exp: "exp", - floor: "floor", - log: "log", - max: "max", - min: "min", - pow: "pow", - random: "random", - round: "round", - sin: "sin", - sqrt: "sqrt", - tan: "tan", - }; - - if (translation[identifier.escapedText as string]) { - const property = tstl.createStringLiteral(translation[identifier.escapedText as string]); - const math = tstl.createIdentifier("math"); - return tstl.createTableIndexExpression(math, property, identifier); - } else { - throw TSTLErrors.UnsupportedProperty("math", identifier.escapedText as string, identifier); - } - } - - // Transpile access of string properties, only supported properties are allowed - public transformStringProperty(node: ts.PropertyAccessExpression): tstl.UnaryExpression { - switch (node.name.escapedText) { - case "length": - return tstl.createUnaryExpression( - this.transformExpression(node.expression), tstl.SyntaxKind.LengthOperator, node); - default: - throw TSTLErrors.UnsupportedProperty("string", node.name.escapedText as string, node); - } - } - - // Transpile access of array properties, only supported properties are allowed - public transformArrayProperty(node: ts.PropertyAccessExpression): tstl.UnaryExpression | undefined { - switch (node.name.escapedText) { - case "length": - return tstl.createUnaryExpression( - this.transformExpression(node.expression), tstl.SyntaxKind.LengthOperator, node); - default: - return undefined; - } - } - - public transformElementAccessExpression(node: ts.ElementAccessExpression): tstl.Expression { - const table = this.transformExpression(node.expression); - const index = this.transformExpression(node.argumentExpression); - - const type = this.checker.getTypeAtLocation(node.expression); - - if (type.symbol && (type.symbol.flags & ts.SymbolFlags.ConstEnum) - && ts.isStringLiteral(node.argumentExpression)) - { - return this.transformConstEnumValue(type, node.argumentExpression.text, node); - } - - if (tsHelper.isArrayType(type, this.checker)) { - return tstl.createTableIndexExpression(table, this.expressionPlusOne(index), node); - } else if (tsHelper.isStringType(type)) { - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier("string"), tstl.createStringLiteral("sub")), - [table, this.expressionPlusOne(index), this.expressionPlusOne(index)], - node - ); - } else { - return tstl.createTableIndexExpression(table, index, node); - } - } - - private transformConstEnumValue(enumType: ts.EnumType, memberName: string, tsOriginal: ts.Node): tstl.Expression { - // Assumption: the enum only has one declaration - const enumDeclaration = enumType.symbol.declarations.find(d => ts.isEnumDeclaration(d)) as ts.EnumDeclaration; - const enumMember = enumDeclaration.members - .find(m => ts.isIdentifier(m.name) && m.name.text === memberName); - - if (enumMember) { - if (enumMember.initializer) { - if (ts.isIdentifier(enumMember.initializer)) { - const [isEnumMember, valueName] = tsHelper.isEnumMember(enumDeclaration, enumMember.initializer); - if (isEnumMember) { - if (ts.isIdentifier(valueName)) { - return this.transformConstEnumValue(enumType, valueName.text, tsOriginal); - } - } else { - return tstl.setNodeOriginal(this.transformExpression(enumMember.initializer), tsOriginal); - } - } else { - return tstl.setNodeOriginal(this.transformExpression(enumMember.initializer), tsOriginal); - } - } else { - let enumValue = 0; - for (const member of enumDeclaration.members) { - if (member === enumMember) { - return tstl.createNumericLiteral(enumValue, tsOriginal); - } - if (member.initializer === undefined) { - enumValue++; - } else if (ts.isNumericLiteral(member.initializer)) { - enumValue = Number(member.initializer.text) + 1; - } - } - - throw TSTLErrors.CouldNotFindEnumMember(enumDeclaration, memberName, tsOriginal); - } - } - throw TSTLErrors.CouldNotFindEnumMember(enumDeclaration, memberName, tsOriginal); - } - - public transformStringCallExpression(node: ts.CallExpression): tstl.Expression { - const expression = node.expression as ts.PropertyAccessExpression; - const params = this.transformArguments(node.arguments); - const caller = this.transformExpression(expression.expression); - - const expressionName = expression.name.escapedText as string; - switch (expressionName) { - case "replace": - return this.transformLuaLibFunction(LuaLibFeature.StringReplace, node, caller, ...params); - case "concat": - return this.transformLuaLibFunction(LuaLibFeature.StringConcat, node, caller, ...params); - case "indexOf": - const stringExpression = - node.arguments.length === 1 - ? this.createStringCall("find", node, caller, params[0]) - : this.createStringCall( - "find", node, caller, params[0], - this.expressionPlusOne(params[1]), - tstl.createBooleanLiteral(true) - ); - - return tstl.createBinaryExpression( - tstl.createBinaryExpression( - stringExpression, - tstl.createNumericLiteral(0), - tstl.SyntaxKind.OrOperator - ), - tstl.createNumericLiteral(1), - tstl.SyntaxKind.SubractionOperator, - node - ); - case "substr": - if (node.arguments.length === 1) { - const arg1 = this.expressionPlusOne(this.transformExpression(node.arguments[0])); - return this.createStringCall("sub", node, caller, arg1); - } else { - const arg1 = params[0]; - const arg2 = params[1]; - const sumArg = tstl.createBinaryExpression(arg1, arg2, tstl.SyntaxKind.AdditionOperator); - return this.createStringCall("sub", node, caller, this.expressionPlusOne(arg1), sumArg); - } - case "substring": - if (node.arguments.length === 1) { - const arg1 = this.expressionPlusOne(params[0]); - return this.createStringCall("sub", node, caller, arg1); - } else { - const arg1 = this.expressionPlusOne(params[0]); - const arg2 = params[1]; - return this.createStringCall("sub", node, caller, arg1, arg2); - } - case "slice": - if (node.arguments.length === 0) { - return caller; - } - else if (node.arguments.length === 1) { - const arg1 = this.expressionPlusOne(params[0]); - return this.createStringCall("sub", node, caller, arg1); - } else { - const arg1 = this.expressionPlusOne(params[0]); - const arg2 = params[1]; - return this.createStringCall("sub", node, caller, arg1, arg2); - } - case "toLowerCase": - return this.createStringCall("lower", node, caller); - case "toUpperCase": - return this.createStringCall("upper", node, caller); - case "split": - return this.transformLuaLibFunction(LuaLibFeature.StringSplit, node, caller, ...params); - case "charAt": - const firstParamPlusOne = this.expressionPlusOne(params[0]); - return this.createStringCall("sub", node, caller, firstParamPlusOne, firstParamPlusOne); - case "charCodeAt": - { - const firstParamPlusOne = this.expressionPlusOne(params[0]); - return this.createStringCall("byte", node, caller, firstParamPlusOne); - } - default: - throw TSTLErrors.UnsupportedProperty("string", expressionName, node); - } - } - - public createStringCall( - methodName: string, - tsOriginal: ts.Node, - ...params: tstl.Expression[] - ): tstl.CallExpression - { - const stringIdentifier = tstl.createIdentifier("string"); - return tstl.createCallExpression( - tstl.createTableIndexExpression(stringIdentifier, tstl.createStringLiteral(methodName)), - params, - tsOriginal - ); - } - - // Transpile a String._ property - public transformStringExpression(identifier: ts.Identifier): ExpressionVisitResult { - const identifierString = identifier.escapedText as string; - - switch (identifierString) { - case "fromCharCode": - return tstl.createTableIndexExpression( - tstl.createIdentifier("string"), - tstl.createStringLiteral("char") - ); - default: - throw TSTLErrors.UnsupportedForTarget( - `string property ${identifierString}`, - this.options.luaTarget, - identifier - ); - } - } - - // Transpile an Object._ property - public transformObjectCallExpression(expression: ts.CallExpression): ExpressionVisitResult { - const method = expression.expression as ts.PropertyAccessExpression; - const parameters = this.transformArguments(expression.arguments); - const caller = this.transformExpression(expression.expression); - const methodName = method.name.escapedText; - - switch (methodName) { - case "assign": - return this.transformLuaLibFunction(LuaLibFeature.ObjectAssign, expression, ...parameters); - case "entries": - return this.transformLuaLibFunction(LuaLibFeature.ObjectEntries, expression, ...parameters); - case "keys": - return this.transformLuaLibFunction(LuaLibFeature.ObjectKeys, expression, ...parameters); - case "values": - return this.transformLuaLibFunction(LuaLibFeature.ObjectValues, expression, ...parameters); - default: - throw TSTLErrors.UnsupportedForTarget( - `object property ${methodName}`, - this.options.luaTarget, - expression - ); - } - } - - // Transpile a Symbol._ property - public transformSymbolCallExpression(expression: ts.CallExpression): tstl.CallExpression { - const method = expression.expression as ts.PropertyAccessExpression; - const parameters = this.transformArguments(expression.arguments); - const methodName = method.name.escapedText; - - switch (methodName) { - case "for": - case "keyFor": - this.importLuaLibFeature(LuaLibFeature.SymbolRegistry); - const upperMethodName = methodName[0].toUpperCase() + methodName.slice(1); - const functionIdentifier = tstl.createIdentifier(`__TS__SymbolRegistry${upperMethodName}`); - return tstl.createCallExpression(functionIdentifier, parameters, expression); - default: - throw TSTLErrors.UnsupportedForTarget( - `symbol property ${methodName}`, - this.options.luaTarget, - expression - ); - } - } - - public transformArrayCallExpression(node: ts.CallExpression): tstl.CallExpression { - const expression = node.expression as ts.PropertyAccessExpression; - const params = this.transformArguments(node.arguments); - const caller = this.transformExpression(expression.expression); - const expressionName = expression.name.escapedText; - switch (expressionName) { - case "concat": - return this.transformLuaLibFunction(LuaLibFeature.ArrayConcat, node, caller, ...params); - case "push": - return this.transformLuaLibFunction(LuaLibFeature.ArrayPush, node, caller, ...params); - case "reverse": - return this.transformLuaLibFunction(LuaLibFeature.ArrayReverse, node, caller); - case "shift": - return this.transformLuaLibFunction(LuaLibFeature.ArrayShift, node, caller); - case "unshift": - return this.transformLuaLibFunction(LuaLibFeature.ArrayUnshift, node, caller, ...params); - case "sort": - return this.transformLuaLibFunction(LuaLibFeature.ArraySort, node, caller); - case "pop": - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier("table"), tstl.createStringLiteral("remove")), - [caller], - node - ); - case "forEach": - return this.transformLuaLibFunction(LuaLibFeature.ArrayForEach, node, caller, ...params); - case "indexOf": - return this.transformLuaLibFunction(LuaLibFeature.ArrayIndexOf, node, caller, ...params); - case "map": - return this.transformLuaLibFunction(LuaLibFeature.ArrayMap, node, caller, ...params); - case "filter": - return this.transformLuaLibFunction(LuaLibFeature.ArrayFilter, node, caller, ...params); - case "some": - return this.transformLuaLibFunction(LuaLibFeature.ArraySome, node, caller, ...params); - case "every": - return this.transformLuaLibFunction(LuaLibFeature.ArrayEvery, node, caller, ...params); - case "slice": - return this.transformLuaLibFunction(LuaLibFeature.ArraySlice, node, caller, ...params); - case "splice": - return this.transformLuaLibFunction(LuaLibFeature.ArraySplice, node, caller, ...params); - case "join": - const parameters = node.arguments.length === 0 - ? [caller, tstl.createStringLiteral(",")] - : [caller].concat(params); - - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier("table"), tstl.createStringLiteral("concat")), - parameters, - node - ); - default: - throw TSTLErrors.UnsupportedProperty("array", expressionName as string, node); - } - } - - public transformFunctionCallExpression(node: ts.CallExpression): tstl.CallExpression { - const expression = node.expression as ts.PropertyAccessExpression; - const callerType = this.checker.getTypeAtLocation(expression.expression); - if (tsHelper.getFunctionContextType(callerType, this.checker) === ContextType.Void) { - throw TSTLErrors.UnsupportedMethodConversion(node); - } - const params = this.transformArguments(node.arguments); - const caller = this.transformExpression(expression.expression); - const expressionName = expression.name.escapedText; - switch (expressionName) { - case "apply": - return this.transformLuaLibFunction(LuaLibFeature.FunctionApply, node, caller, ...params); - case "bind": - return this.transformLuaLibFunction(LuaLibFeature.FunctionBind, node, caller, ...params); - case "call": - return this.transformLuaLibFunction(LuaLibFeature.FunctionCall, node, caller, ...params); - default: - throw TSTLErrors.UnsupportedProperty("function", expressionName as string, node); - } - } - - public transformArrayBindingElement(name: ts.ArrayBindingElement): tstl.Identifier { - if (ts.isOmittedExpression(name)) { - return tstl.createIdentifier("__", name); - } else if (ts.isIdentifier(name)) { - return this.transformIdentifier(name); - } else if (ts.isBindingElement(name) && ts.isIdentifier(name.name)) { - return this.transformIdentifier(name.name); - } else { - throw TSTLErrors.UnsupportedKind("array binding element", name.kind, name); - } - } - - public transformAssertionExpression(node: ts.AssertionExpression): tstl.Expression { - this.validateFunctionAssignment( - node, - this.checker.getTypeAtLocation(node.expression), - this.checker.getTypeAtLocation(node.type) - ); - return this.transformExpression(node.expression); - } - - public transformTypeOfExpression(node: ts.TypeOfExpression): ExpressionVisitResult { - const expression = this.transformExpression(node.expression); - const typeFunctionIdentifier = tstl.createIdentifier("type"); - const typeCall = tstl.createCallExpression(typeFunctionIdentifier, [expression]); - const tableString = tstl.createStringLiteral("table"); - const objectString = tstl.createStringLiteral("object"); - const condition = tstl.createBinaryExpression(typeCall, tableString, tstl.SyntaxKind.EqualityOperator); - const andClause = tstl.createBinaryExpression(condition, objectString, tstl.SyntaxKind.AndOperator); - - return tstl.createBinaryExpression( - andClause, - tstl.cloneNode(typeCall), - tstl.SyntaxKind.OrOperator, - node - ); - } - - public transformSpreadElement(expression: ts.SpreadElement): ExpressionVisitResult { - const innerExpression = this.transformExpression(expression.expression); - - return this.createUnpackCall(innerExpression, expression); - } - - public transformStringLiteral(literal: ts.StringLiteralLike): tstl.StringLiteral { - const text = tsHelper.escapeString(literal.text); - return tstl.createStringLiteral(text); - } - - public transformNumericLiteral(literal: ts.NumericLiteral): tstl.NumericLiteral { - const value = Number(literal.text); - return tstl.createNumericLiteral(value, literal); - } - - public transformTrueKeyword(trueKeyword: ts.BooleanLiteral): tstl.BooleanLiteral { - return tstl.createBooleanLiteral(true, trueKeyword); - } - - public transformFalseKeyword(falseKeyword: ts.BooleanLiteral): tstl.BooleanLiteral { - return tstl.createBooleanLiteral(false, falseKeyword); - } - - public transformNullOrUndefinedKeyword(originalNode: ts.Node): tstl.NilLiteral { - return tstl.createNilLiteral(originalNode); - } - - public transformThisKeyword(thisKeyword: ts.ThisExpression): tstl.Expression { - return this.createSelfIdentifier(thisKeyword); - } - - public transformTemplateExpression(expression: ts.TemplateExpression): tstl.BinaryExpression { - const parts: tstl.Expression[] = [tstl.createStringLiteral(tsHelper.escapeString(expression.head.text))]; - expression.templateSpans.forEach(span => { - const expr = this.transformExpression(span.expression); - const text = tstl.createStringLiteral(tsHelper.escapeString(span.literal.text)); - - // tostring(expr).."text" - parts.push(tstl.createBinaryExpression( - tstl.createCallExpression(tstl.createIdentifier("tostring"), [expr]), - text, - tstl.SyntaxKind.ConcatOperator) - ); - }); - return parts.reduce((prev, current) => tstl.createBinaryExpression( - prev, - current, - tstl.SyntaxKind.ConcatOperator) - ) as tstl.BinaryExpression; - } - - public transformPropertyName(propertyName: ts.PropertyName): tstl.Expression { - if (ts.isComputedPropertyName(propertyName)) { - return this.transformExpression(propertyName.expression); - } else if (ts.isStringLiteral(propertyName)) { - return this.transformStringLiteral(propertyName); - } else if (ts.isNumericLiteral(propertyName)) { - const value = Number(propertyName.text); - return tstl.createNumericLiteral(value, propertyName); - } else { - return tstl.createStringLiteral(this.transformIdentifier(propertyName).text); - } - } - - public transformIdentifier(expression: ts.Identifier): tstl.Identifier { - if (expression.originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) { - return tstl.createIdentifier("nil"); // TODO this is a hack that allows use to keep Identifier - // as return time as changing that would break a lot of stuff. - // But this should be changed to retun tstl.createNilLiteral() - // at some point. - } - - let escapedText = expression.escapedText as string; - const underScoreCharCode = "_".charCodeAt(0); - if (escapedText.length >= 3 && escapedText.charCodeAt(0) === underScoreCharCode && - escapedText.charCodeAt(1) === underScoreCharCode && escapedText.charCodeAt(2) === underScoreCharCode) { - escapedText = escapedText.substr(1); - } - - if (this.luaKeywords.has(escapedText)) { - throw TSTLErrors.KeywordIdentifier(expression); - } - - const symbolId = this.getIdentifierSymbolId(expression); - return tstl.createIdentifier(escapedText, expression, symbolId); - } - - public transformIdentifierExpression(expression: ts.Identifier): tstl.IdentifierOrTableIndexExpression { - const identifier = this.transformIdentifier(expression); - if (this.isIdentifierExported(identifier)) { - return this.createExportedIdentifier(identifier); - } - return identifier; - } - - public isIdentifierExported(identifier: tstl.Identifier): boolean { - if (!this.isModule && !this.currentNamespace) { - return false; - } - - const symbolInfo = identifier.symbolId && this.symbolInfo.get(identifier.symbolId); - if (!symbolInfo) { - return false; - } - - const currentScope = this.currentNamespace ? this.currentNamespace : this.currentSourceFile; - const scopeSymbol = this.checker.getSymbolAtLocation(currentScope) - ? this.checker.getSymbolAtLocation(currentScope) - : this.checker.getTypeAtLocation(currentScope).getSymbol(); - - const it: Iterable = { - [Symbol.iterator]: () => scopeSymbol.exports.values(), // Why isn't ts.SymbolTable.values() iterable? - }; - for (const symbol of it) { - if (symbol === symbolInfo.symbol) { - return true; - } - } - return false; - } - - public addExportToIdentifier(identifier: tstl.Identifier): tstl.IdentifierOrTableIndexExpression { - if (this.isIdentifierExported(identifier)) { - return this.createExportedIdentifier(identifier); - } - return identifier; - } - - public createExportedIdentifier(identifier: tstl.Identifier): tstl.TableIndexExpression { - const exportTable = this.currentNamespace - ? this.transformIdentifier(this.currentNamespace.name as ts.Identifier) - : tstl.createIdentifier("exports"); - - return tstl.createTableIndexExpression( - exportTable, - tstl.createStringLiteral(identifier.text)); - } - - public transformLuaLibFunction( - func: LuaLibFeature, - tsParent: ts.Expression, - ...params: tstl.Expression[] - ): tstl.CallExpression - { - this.importLuaLibFeature(func); - const functionIdentifier = tstl.createIdentifier(`__TS__${func}`); - return tstl.createCallExpression(functionIdentifier, params, tsParent); - } - - public checkForLuaLibType(type: ts.Type): void { - if (type.symbol) { - switch (this.checker.getFullyQualifiedName(type.symbol)) { - case "Map": - this.importLuaLibFeature(LuaLibFeature.Map); - return; - case "Set": - this.importLuaLibFeature(LuaLibFeature.Set); - return; - case "WeakMap": - this.importLuaLibFeature(LuaLibFeature.WeakMap); - return; - case "WeakSet": - this.importLuaLibFeature(LuaLibFeature.WeakSet); - return; - } - } - } - - public importLuaLibFeature(feature: LuaLibFeature): void { - // Add additional lib requirements - if (feature === LuaLibFeature.Map || feature === LuaLibFeature.Set) { - this.luaLibFeatureSet.add(LuaLibFeature.InstanceOf); - } - - this.luaLibFeatureSet.add(feature); - } - - public createImmediatelyInvokedFunctionExpression( - statements: tstl.Statement[], - result: tstl.Expression | tstl.Expression[], - tsOriginal: ts.Node - ): tstl.CallExpression - { - const body = statements ? statements.slice(0) : []; - body.push(tstl.createReturnStatement(Array.isArray(result) ? result : [result])); - const iife = tstl.createFunctionExpression(tstl.createBlock(body)); - return tstl.createCallExpression(tstl.createParenthesizedExpression(iife), [], tsOriginal); - } - - public createUnpackCall(expression: tstl.Expression, tsOriginal: ts.Node): tstl.Expression { - switch (this.options.luaTarget) { - case LuaTarget.Lua51: - case LuaTarget.LuaJIT: - return tstl.createCallExpression(tstl.createIdentifier("unpack"), [expression], tsOriginal); - - case LuaTarget.Lua52: - case LuaTarget.Lua53: - default: - return tstl.createCallExpression( - tstl.createTableIndexExpression(tstl.createIdentifier("table"), tstl.createStringLiteral("unpack")), - [expression], - tsOriginal - ); - } - } - - private getAbsoluteImportPath(relativePath: string): string { - if (relativePath.charAt(0) !== "." && this.options.baseUrl) { - return path.resolve(this.options.baseUrl, relativePath); - } - return path.resolve(path.dirname(this.currentSourceFile.fileName), relativePath); - } - - private getImportPath(relativePath: string): string { - const rootDir = this.options.rootDir ? path.resolve(this.options.rootDir) : path.resolve("."); - const absoluteImportPath = path.format(path.parse(this.getAbsoluteImportPath(relativePath))); - const absoluteRootDirPath = path.format(path.parse(rootDir)); - if (absoluteImportPath.includes(absoluteRootDirPath)) { - return this.formatPathToLuaPath( - absoluteImportPath.replace(absoluteRootDirPath, "").slice(1)); - } else { - throw TSTLErrors.UnresolvableRequirePath(undefined, - `Cannot create require path. Module does not exist within --rootDir`, - relativePath); - } - } - - private formatPathToLuaPath(filePath: string): string { - filePath = filePath.replace(/\.json$/, ""); - if (process.platform === "win32") { - // Windows can use backslashes - filePath = filePath - .replace(/\.\\/g, "") - .replace(/\\/g, "."); - } - return filePath - .replace(/\.\//g, "") - .replace(/\//g, "."); - } - - private shouldExportIdentifier(identifier: tstl.Identifier | tstl.Identifier[]): boolean { - if (!this.isModule && !this.currentNamespace) { - return false; - } - if (Array.isArray(identifier)) { - return identifier.some(i => this.isIdentifierExported(i)); - } else { - return this.isIdentifierExported(identifier); - } - } - - private createSelfIdentifier(tsOriginal?: ts.Node): tstl.Identifier { - return tstl.createIdentifier("self", tsOriginal); - } - - private createLocalOrExportedOrGlobalDeclaration( - lhs: tstl.Identifier | tstl.Identifier[], - rhs?: tstl.Expression, - tsOriginal?: ts.Node, - parent?: tstl.Node - ): tstl.Statement[] - { - let declaration: tstl.VariableDeclarationStatement | undefined; - let assignment: tstl.AssignmentStatement | undefined; - - const functionDeclaration = tsOriginal && ts.isFunctionDeclaration(tsOriginal) ? tsOriginal : undefined; - - if (this.shouldExportIdentifier(lhs)) { - // exported - if (!rhs) { - return []; - - } else if (Array.isArray(lhs)) { - assignment = tstl.createAssignmentStatement( - lhs.map(i => this.createExportedIdentifier(i)), - rhs, - tsOriginal, - parent - ); - - } else { - assignment = tstl.createAssignmentStatement( - this.createExportedIdentifier(lhs), - rhs, - tsOriginal, - parent - ); - } - - } else { - const insideFunction = this.findScope(ScopeType.Function) !== undefined; - let isLetOrConst = false; - let isFirstDeclaration = true; // var can have multiple declarations for the same variable :/ - if (tsOriginal && ts.isVariableDeclaration(tsOriginal) && tsOriginal.parent) { - isLetOrConst = (tsOriginal.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; - isFirstDeclaration = isLetOrConst || tsHelper.isFirstDeclaration(tsOriginal, this.checker); - } - if ((this.isModule || this.currentNamespace || insideFunction || isLetOrConst) && isFirstDeclaration) { - // local - const isFunctionType = functionDeclaration - || (tsOriginal && ts.isVariableDeclaration(tsOriginal) && tsOriginal.initializer - && tsHelper.isFunctionTypeAtLocation(tsOriginal.initializer, this.checker)); - if (isFunctionType) { - // Split declaration and assignment for cuntions to allow recursion - declaration = tstl.createVariableDeclarationStatement(lhs, undefined, tsOriginal, parent); - assignment = tstl.createAssignmentStatement(lhs, rhs, tsOriginal, parent); - - } else { - declaration = tstl.createVariableDeclarationStatement(lhs, rhs, tsOriginal, parent); - } - - if (!this.options.noHoisting) { - // Remember local variable declarations for hoisting later - const scope = isLetOrConst || functionDeclaration - ? this.peekScope() - : this.findScope(ScopeType.Function | ScopeType.File); - - if (!scope.variableDeclarations) { scope.variableDeclarations = []; } - scope.variableDeclarations.push(declaration); - } - - } else if (rhs) { - // global - assignment = tstl.createAssignmentStatement(lhs, rhs, tsOriginal, parent); - - } else { - return []; - } - } - - if (!this.options.noHoisting && functionDeclaration) { - // Remember function definitions for hoisting later - const functionSymbolId = (lhs as tstl.Identifier).symbolId; - if (functionSymbolId !== undefined) { - this.peekScope().functionDefinitions.get(functionSymbolId).assignment = assignment; - } - } - - if (declaration && assignment) { - return [declaration, assignment]; - } else if (declaration) { - return [declaration]; - } else { - return [assignment]; - } - } - - private validateFunctionAssignment(node: ts.Node, fromType: ts.Type, toType: ts.Type, toName?: string): void { - if (toType === fromType) { - return; - } - - if ((toType.flags & ts.TypeFlags.Any) !== 0) { - // Assigning to un-typed variable - return; - } - - // Use cache to avoid repeating check for same types (protects against infinite loop in recursive types) - let fromTypeCache = this.typeValidationCache.get(fromType); - if (fromTypeCache) { - if (fromTypeCache.has(toType)) { - return; - } - } else { - fromTypeCache = new Set(); - this.typeValidationCache.set(fromType, fromTypeCache); - } - fromTypeCache.add(toType); - - // Check function assignments - const fromContext = tsHelper.getFunctionContextType(fromType, this.checker); - const toContext = tsHelper.getFunctionContextType(toType, this.checker); - - if (fromContext === ContextType.Mixed || toContext === ContextType.Mixed) { - throw TSTLErrors.UnsupportedOverloadAssignment(node, toName); - } else if (fromContext !== toContext && fromContext !== ContextType.None && toContext !== ContextType.None) { - if (toContext === ContextType.Void) { - throw TSTLErrors.UnsupportedFunctionConversion(node, toName); - } else { - throw TSTLErrors.UnsupportedMethodConversion(node, toName); - } - } - - const fromTypeNode = this.checker.typeToTypeNode(fromType); - const toTypeNode = this.checker.typeToTypeNode(toType); - - if ((ts.isArrayTypeNode(toTypeNode) || ts.isTupleTypeNode(toTypeNode)) - && (ts.isArrayTypeNode(fromTypeNode) || ts.isTupleTypeNode(fromTypeNode))) { - // Recurse into arrays/tuples - const fromTypeReference = fromType as ts.TypeReference; - const toTypeReference = toType as ts.TypeReference; - const count = Math.min(fromTypeReference.typeArguments.length, toTypeReference.typeArguments.length); - for (let i = 0; i < count; ++i) { - this.validateFunctionAssignment( - node, - fromTypeReference.typeArguments[i], - toTypeReference.typeArguments[i], - toName - ); - } - } - - if ((toType.flags & ts.TypeFlags.Object) !== 0 - && ((toType as ts.ObjectType).objectFlags & ts.ObjectFlags.ClassOrInterface) !== 0 - && toType.symbol && toType.symbol.members && fromType.symbol && fromType.symbol.members) - { - // Recurse into interfaces - toType.symbol.members.forEach((toMember, memberName) => { - const fromMember = fromType.symbol.members.get(memberName); - if (fromMember) { - const toMemberType = this.checker.getTypeOfSymbolAtLocation(toMember, node); - const fromMemberType = this.checker.getTypeOfSymbolAtLocation(fromMember, node); - this.validateFunctionAssignment( - node, fromMemberType, toMemberType, toName ? `${toName}.${memberName}` : memberName.toString()); - } - }); - } - } - - private wrapInFunctionCall(expression: tstl.Expression): tstl.FunctionExpression { - const returnStatement = tstl.createReturnStatement([expression]); - return tstl.createFunctionExpression(tstl.createBlock([returnStatement])); - } - - private wrapInTable(...expressions: tstl.Expression[]): tstl.ParenthesizedExpression { - const fields = expressions.map(e => tstl.createTableFieldExpression(e)); - return tstl.createParenthesizedExpression(tstl.createTableExpression(fields)); - } - - private expressionPlusOne(expression: tstl.Expression): tstl.BinaryExpression { - return tstl.createBinaryExpression(expression, tstl.createNumericLiteral(1), tstl.SyntaxKind.AdditionOperator); - } - - private getIdentifierSymbolId(identifier: ts.Identifier): tstl.SymbolId { - const symbol = this.checker.getSymbolAtLocation(identifier); - let symbolId: number | undefined; - if (symbol) { - // Track first time symbols are seen - if (!this.symbolIds.has(symbol)) { - symbolId = this.genSymbolIdCounter++; - const symbolInfo: SymbolInfo = {symbol, firstSeenAtPos: identifier.pos}; - this.symbolIds.set(symbol, symbolId); - this.symbolInfo.set(symbolId, symbolInfo); - } else { - symbolId = this.symbolIds.get(symbol); - } - - if (this.options.noHoisting) { - // Check for reference-before-declaration - const declaration = tsHelper.getFirstDeclaration(symbol, this.currentSourceFile); - if (declaration && identifier.pos < declaration.pos) { - throw TSTLErrors.ReferencedBeforeDeclaration(identifier); - } - - } else { - //Mark symbol as seen in all current scopes - this.scopeStack.forEach(s => { - if (!s.referencedSymbols) { s.referencedSymbols = new Set(); } - s.referencedSymbols.add(symbolId); - }); - } - } - return symbolId; - } - - protected findScope(scopeTypes: ScopeType): Scope | undefined { - return this.scopeStack.slice().reverse().find(s => (scopeTypes & s.type) !== 0); - } - - protected peekScope(): Scope { - return this.scopeStack[this.scopeStack.length - 1]; - } - - protected pushScope(scopeType: ScopeType, node: ts.Node): void { - this.scopeStack.push({ - type: scopeType, - id: this.genVarCounter, - }); - this.genVarCounter++; - } - - private shouldHoist(symbolId: tstl.SymbolId, scope: Scope): boolean { - const symbolInfo = this.symbolInfo.get(symbolId); - if (!symbolInfo) { - return false; - } - - const declaration = tsHelper.getFirstDeclaration(symbolInfo.symbol, this.currentSourceFile); - if (!declaration) { - return false; - } - - if (symbolInfo.firstSeenAtPos < declaration.pos) { - return true; - } - - if (scope.functionDefinitions) { - for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { - if (functionSymbolId !== symbolId // Don't recurse into self - && declaration.pos < functionDefinition.assignment.pos // Ignore functions before symbol declaration - && functionDefinition.referencedSymbols.has(symbolId) - && this.shouldHoist(functionSymbolId, scope)) - { - return true; - } - } - } - - return false; - } - - protected replaceStatementInParent(oldNode: tstl.Statement, newNode?: tstl.Statement): void { - if (!oldNode.parent) { - throw new Error("node has not yet been assigned a parent"); - } - - if (tstl.isBlock(oldNode.parent) || tstl.isDoStatement(oldNode.parent)) { - if (newNode) { - oldNode.parent.statements.splice(oldNode.parent.statements.indexOf(oldNode), 1, newNode); - } else { - oldNode.parent.statements.splice(oldNode.parent.statements.indexOf(oldNode), 1); - } - } else { - throw new Error("unexpected parent type"); - } - } - - protected hoistFunctionDefinitions(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] { - if (!scope.functionDefinitions) { - return statements; - } - - const result = statements.slice(); - const hoistedFunctions: tstl.AssignmentStatement[] = []; - for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { - if (this.shouldHoist(functionSymbolId, scope)) { - const i = result.indexOf(functionDefinition.assignment); - result.splice(i, 1); - hoistedFunctions.push(functionDefinition.assignment); - } - } - if (hoistedFunctions.length > 0) { - result.unshift(...hoistedFunctions); - } - return result; - } - - protected hoistVariableDeclarations(scope: Scope, statements: tstl.Statement[]): tstl.Statement[] { - if (!scope.variableDeclarations) { - return statements; - } - - const result = statements.slice(); - const hoistedLocals: tstl.Identifier[] = []; - for (const declaration of scope.variableDeclarations) { - const symbols = declaration.left.map(i => i.symbolId).filter(s => s !== undefined); - if (symbols.some(s => this.shouldHoist(s, scope))) { - let assignment: tstl.AssignmentStatement | undefined; - if (declaration.right) { - assignment = tstl.createAssignmentStatement( - declaration.left, - declaration.right - ); - } - const i = result.indexOf(declaration); - if (i >= 0) { - if (assignment) { - result.splice(i, 1, assignment); - } else { - result.splice(i, 1); - } - } else { - // Special case for 'var's declared in child scopes - this.replaceStatementInParent(declaration, assignment); - } - hoistedLocals.push(...declaration.left); - } - } - if (hoistedLocals.length > 0) { - result.unshift(tstl.createVariableDeclarationStatement(hoistedLocals)); - } - return result; - } - - protected performHoisting(statements: tstl.Statement[]): tstl.Statement[] { - if (this.options.noHoisting) { - return statements; - } - - const scope = this.peekScope(); - - let result = this.hoistFunctionDefinitions(scope, statements); - - result = this.hoistVariableDeclarations(scope, result); - - return result; - } - - protected popScope(): Scope { - const scope = this.scopeStack.pop(); - return scope; - } - - protected createHoistableVariableDeclarationStatement( - identifier: ts.Identifier, - initializer?: tstl.Expression, - tsOriginal?: ts.Node, - parent?: tstl.Node - ): tstl.AssignmentStatement | tstl.VariableDeclarationStatement - { - const variable = this.transformIdentifier(identifier); - const declaration = tstl.createVariableDeclarationStatement(variable, initializer, tsOriginal, parent); - if (!this.options.noHoisting && variable.symbolId) { - const scope = this.peekScope(); - if (!scope.variableDeclarations) { scope.variableDeclarations = []; } - scope.variableDeclarations.push(declaration); - } - return declaration; - } - - private statementVisitResultToStatementArray(visitResult: StatementVisitResult): tstl.Statement[] { - if (!Array.isArray(visitResult)) { - if (visitResult) { - return [visitResult]; - } - return []; - } - const flatten = (arr, result = []) => { - for (let i = 0, length = arr.length; i < length; i++) { - const value = arr[i]; - if (Array.isArray(value)) { - flatten(value, result); - } else if (value) { - // ignore value if undefined - result.push(value); - } - } - return result; - }; - return flatten(visitResult); - } -} diff --git a/src/LuaTranspiler.ts b/src/LuaTranspiler.ts deleted file mode 100644 index a4a4416a2..000000000 --- a/src/LuaTranspiler.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; - -import * as tstl from "./LuaAST"; - -import {CompilerOptions, LuaLibImportKind, LuaTarget} from "./CompilerOptions"; -import {LuaPrinter} from "./LuaPrinter"; -import {LuaTransformer} from "./LuaTransformer"; - -export class LuaTranspiler { - private program: ts.Program; - - private options: CompilerOptions; - - private luaTransformer: LuaTransformer; - - private luaPrinter: LuaPrinter; - - constructor(program: ts.Program) { - this.program = program; - this.options = this.getOptions(program); - this.luaTransformer = new LuaTransformer(this.program, this.options); - this.luaPrinter = new LuaPrinter(this.options); - } - - private getOptions(program: ts.Program): CompilerOptions { - const options = program.getCompilerOptions() as CompilerOptions; - - // Make options case-insenstive - if (options.luaTarget) { - options.luaTarget = options.luaTarget.toLowerCase() as LuaTarget; - } - if (options.luaLibImport) { - options.luaLibImport = options.luaLibImport.toLocaleLowerCase() as LuaLibImportKind; - } - - return options; - } - - private reportErrors(): number { - // Get all diagnostics, ignore unsupported extension - const diagnostics = ts.getPreEmitDiagnostics(this.program).filter(diag => diag.code !== 6054); - diagnostics.forEach(diag => this.reportDiagnostic(diag)); - - // If there are errors dont emit - if (diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error).length > 0) { - if (!this.options.watch) { - process.exit(1); - } else { - return 1; - } - } - - return 0; - } - - public emitLuaLib(): string { - const outPath = path.join(this.options.outDir, "lualib_bundle.lua"); - fs.copyFileSync( - path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), - outPath - ); - return outPath; - } - - public emitFilesAndReportErrors(): number { - const error = this.reportErrors(); - if (error > 0) { - return error; - } - - this.program.getSourceFiles().forEach(sourceFile => { - this.emitSourceFile(sourceFile); - }); - - // Copy lualib to target dir - if (this.options.luaLibImport === LuaLibImportKind.Require - || this.options.luaLibImport === LuaLibImportKind.Always - ) { - this.emitLuaLib(); - } - - return 0; - } - - public emitSourceFile(sourceFile: ts.SourceFile): void { - if (!sourceFile.isDeclarationFile) { - try { - const rootDir = this.options.rootDir; - - const lua = this.transpileSourceFile(sourceFile); - - let outPath = sourceFile.fileName; - if (this.options.outDir !== this.options.rootDir) { - const relativeSourcePath = path.resolve(sourceFile.fileName).replace(path.resolve(rootDir), ""); - outPath = path.join(this.options.outDir, relativeSourcePath); - } - - // change extension or rename to outFile - if (this.options.outFile) { - if (path.isAbsolute(this.options.outFile)) { - outPath = this.options.outFile; - } else { - // append to workingDir or outDir - outPath = path.resolve(this.options.outDir, this.options.outFile); - } - } else { - const fileNameLua = path.basename(outPath, path.extname(outPath)) + ".lua"; - outPath = path.join(path.dirname(outPath), fileNameLua); - } - - // Write output - ts.sys.writeFile(outPath, lua); - } catch (exception) { - /* istanbul ignore else: Testing else part would require to add a bug/exception to our code */ - if (exception.node) { - const pos = ts.getLineAndCharacterOfPosition(sourceFile, exception.node.pos); - // Graciously handle transpilation errors - console.error("Encountered error parsing file: " + exception.message); - console.error(`${sourceFile.fileName} (${1 + pos.line},${pos.character})\n${exception.stack}`); - } else { - throw exception; - } - } - } - } - - public transpileSourceFile(sourceFile: ts.SourceFile): string { - // Transform AST - const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile); - // Print AST - return this.luaPrinter.print(luaAST, lualibFeatureSet); - } - - public transpileSourceFileKeepAST(sourceFile: ts.SourceFile): [tstl.Block, string] { - // Transform AST - const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile); - // Print AST - return [luaAST, this.luaPrinter.print(luaAST, lualibFeatureSet)]; - } - - public reportDiagnostic(diagnostic: ts.Diagnostic): void { - if (diagnostic.file) { - const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); - const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); - console.log(`${diagnostic.code}: ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); - } else { - console.log(`${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`); - } - } -} diff --git a/src/TSHelper.ts b/src/TSHelper.ts deleted file mode 100644 index e67dfd969..000000000 --- a/src/TSHelper.ts +++ /dev/null @@ -1,690 +0,0 @@ -import * as ts from "typescript"; - -import {Decorator, DecoratorKind} from "./Decorator"; -import * as tstl from "./LuaAST"; - -export enum ContextType { - None, - Void, - NonVoid, - Mixed, -} - -const defaultArrayCallMethodNames = new Set([ - "concat", - "push", - "reverse", - "shift", - "unshift", - "sort", - "pop", - "forEach", - "indexOf", - "map", - "filter", - "some", - "every", - "slice", - "splice", - "join", -]); - -const defaultArrayPropertyNames = new Set([ - "length", -]); - -export class TSHelper { - // Reverse lookup of enum key by value - public static enumName(needle: T, haystack: any): string { - for (const name in haystack) { - if (haystack[name] === needle) { - return name; - } - } - return "unknown"; - } - - // Breaks down a mask into all flag names. - public static enumNames(mask: number, haystack: any): string[] { - const result = []; - for (const name in haystack) { - if ((mask & haystack[name]) !== 0 && mask >= haystack[name]) { - result.push(name); - } - } - return result; - } - - public static getExtendedTypeNode(node: ts.ClassLikeDeclarationBase, checker: ts.TypeChecker): - ts.ExpressionWithTypeArguments | undefined { - if (node && node.heritageClauses) { - for (const clause of node.heritageClauses) { - if (clause.token === ts.SyntaxKind.ExtendsKeyword) { - const superType = checker.getTypeAtLocation(clause.types[0]); - const decorators = TSHelper.getCustomDecorators(superType, checker); - if (!decorators.has(DecoratorKind.PureAbstract)) { - return clause.types[0]; - } - } - } - } - return undefined; - } - - public static getExtendedType(node: ts.ClassLikeDeclarationBase, checker: ts.TypeChecker): ts.Type | undefined { - const extendedTypeNode = TSHelper.getExtendedTypeNode(node, checker); - return extendedTypeNode && checker.getTypeAtLocation(extendedTypeNode); - } - - public static isFileModule(sourceFile: ts.SourceFile): boolean { - if (sourceFile) { - return sourceFile.statements.some(TSHelper.isStatementExported); - } - return false; - } - - public static isStatementExported(statement: ts.Statement): boolean { - if (ts.isExportAssignment(statement) || ts.isExportDeclaration(statement)) { - return true; - } - if (ts.isVariableStatement(statement)) { - return statement.declarationList.declarations.some( - declaration => (ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Export) !== 0); - } - return TSHelper.isDeclaration(statement) - && ((ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Export) !== 0); - } - - public static isDeclaration(node: ts.Node): node is ts.Declaration { - return ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) || ts.isExportDeclaration(node) - || ts.isImportDeclaration(node) || ts.isMethodDeclaration(node) || ts.isModuleDeclaration(node) - || ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node) || ts.isInterfaceDeclaration(node) - || ts.isTypeAliasDeclaration(node) || ts.isNamespaceExportDeclaration(node); - } - - public static isInDestructingAssignment(node: ts.Node): boolean { - return node.parent && ((ts.isVariableDeclaration(node.parent) && ts.isArrayBindingPattern(node.parent.name)) || - (ts.isBinaryExpression(node.parent) && ts.isArrayLiteralExpression(node.parent.left))); - } - - // iterate over a type and its bases until the callback returns true. - public static forTypeOrAnySupertype( - type: ts.Type, - checker: ts.TypeChecker, - predicate: (type: ts.Type) => boolean - ): boolean - { - if (predicate(type)) { - return true; - } - if (!type.isClassOrInterface() && type.symbol) { - type = checker.getDeclaredTypeOfSymbol(type.symbol); - } - const superTypes = type.getBaseTypes(); - if (superTypes) { - for (const superType of superTypes) { - if (TSHelper.forTypeOrAnySupertype(superType, checker, predicate)) { - return true; - } - } - } - return false; - } - - public static isStatic(node: ts.Node): boolean { - return node.modifiers !== undefined && node.modifiers.some(m => m.kind === ts.SyntaxKind.StaticKeyword); - } - - public static isStringType(type: ts.Type): boolean { - return (type.flags & ts.TypeFlags.String) !== 0 || (type.flags & ts.TypeFlags.StringLike) !== 0 || - (type.flags & ts.TypeFlags.StringLiteral) !== 0; - } - - public static isArrayTypeNode(typeNode: ts.TypeNode): boolean { - return typeNode.kind === ts.SyntaxKind.ArrayType || typeNode.kind === ts.SyntaxKind.TupleType || - ((typeNode.kind === ts.SyntaxKind.UnionType || typeNode.kind === ts.SyntaxKind.IntersectionType) && - (typeNode as ts.UnionOrIntersectionTypeNode).types.some(TSHelper.isArrayTypeNode)); - } - - public static isExplicitArrayType(type: ts.Type, checker: ts.TypeChecker): boolean { - const typeNode = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.InTypeAlias); - return typeNode && TSHelper.isArrayTypeNode(typeNode); - } - - public static isFunctionType(type: ts.Type, checker: ts.TypeChecker): boolean { - const typeNode = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.InTypeAlias); - return typeNode && ts.isFunctionTypeNode(typeNode); - } - - public static isFunctionTypeAtLocation(node: ts.Node, checker: ts.TypeChecker): boolean { - const type = checker.getTypeAtLocation(node); - return TSHelper.isFunctionType(type, checker); - } - - public static isArrayType(type: ts.Type, checker: ts.TypeChecker): boolean { - return TSHelper.forTypeOrAnySupertype(type, checker, t => TSHelper.isExplicitArrayType(t, checker)); - } - - public static isLuaIteratorCall(node: ts.Node, checker: ts.TypeChecker): boolean { - if (ts.isCallExpression(node) && node.parent && ts.isForOfStatement(node.parent)) { - const type = checker.getTypeAtLocation(node.expression); - return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.LuaIterator); - } else { - return false; - } - } - - public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean { - if (ts.isCallExpression(node)) { - const type = checker.getTypeAtLocation(node.expression); - - return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.TupleReturn); - } else { - return false; - } - } - - public static isInTupleReturnFunction(node: ts.Node, checker: ts.TypeChecker): boolean { - const declaration = TSHelper.findFirstNodeAbove(node, ts.isFunctionLike); - if (declaration) { - const decorators = TSHelper.getCustomDecorators(checker.getTypeAtLocation(declaration), checker); - return decorators.has(DecoratorKind.TupleReturn); - } else { - return false; - } - } - - public static isInLuaIteratorFunction(node: ts.Node, checker: ts.TypeChecker): boolean { - const declaration = TSHelper.findFirstNodeAbove(node, ts.isFunctionLike); - if (declaration) { - const decorators = TSHelper.getCustomDecorators(checker.getTypeAtLocation(declaration), checker); - return decorators.has(DecoratorKind.LuaIterator); - } else { - return false; - } - } - - public static getContainingFunctionReturnType(node: ts.Node, checker: ts.TypeChecker): ts.Type { - const declaration = TSHelper.findFirstNodeAbove(node, ts.isFunctionLike); - if (declaration) { - const signature = checker.getSignatureFromDeclaration(declaration); - return checker.getReturnTypeOfSignature(signature); - } - return undefined; - } - - public static collectCustomDecorators( - symbol: ts.Symbol, - checker: ts.TypeChecker, - decMap: Map - ): void - { - const comments = symbol.getDocumentationComment(checker); - const decorators = comments.filter(comment => comment.kind === "text") - .map(comment => comment.text.split("\n")) - .reduce((a, b) => a.concat(b), []) - .map(line => line.trim()) - .filter(comment => comment[0] === "!"); - - decorators.forEach(decStr => { - const [decoratorName, ...decoratorArguments] = decStr.split(" "); - if (Decorator.isValid(decoratorName.substr(1))) { - const dec = new Decorator(decoratorName.substr(1), decoratorArguments); - decMap.set(dec.kind, dec); - console.warn( - `[Deprecated] Decorators with ! are being deprecated, ` + - `use @${decStr.substr(1)} instead`); - } else { - console.warn(`Encountered unknown decorator ${decStr}.`); - } - }); - symbol.getJsDocTags().forEach(tag => { - if (Decorator.isValid(tag.name)) { - const dec = new Decorator(tag.name, tag.text ? tag.text.split(" ") : []); - decMap.set(dec.kind, dec); - } - }); - } - - public static getCustomDecorators(type: ts.Type, checker: ts.TypeChecker): Map { - const decMap = new Map(); - if (type.symbol) { - TSHelper.collectCustomDecorators(type.symbol, checker, decMap); - } - if (type.aliasSymbol) { - TSHelper.collectCustomDecorators(type.aliasSymbol, checker, decMap); - } - return decMap; - } - - // Search up until finding a node satisfying the callback - public static findFirstNodeAbove(node: ts.Node, callback: (n: ts.Node) => n is T): T { - let current = node; - while (current.parent) { - if (callback(current.parent)) { - return current.parent; - } else { - current = current.parent; - } - } - return undefined; - } - - public static isBinaryAssignmentToken(token: ts.SyntaxKind): [boolean, tstl.BinaryOperator] { - switch (token) { - case ts.SyntaxKind.BarEqualsToken: - return [true, tstl.SyntaxKind.BitwiseOrOperator]; - case ts.SyntaxKind.PlusEqualsToken: - return [true, tstl.SyntaxKind.AdditionOperator]; - case ts.SyntaxKind.CaretEqualsToken: - return [true, tstl.SyntaxKind.BitwiseExclusiveOrOperator]; - case ts.SyntaxKind.MinusEqualsToken: - return [true, tstl.SyntaxKind.SubractionOperator]; - case ts.SyntaxKind.SlashEqualsToken: - return [true, tstl.SyntaxKind.DivisionOperator]; - case ts.SyntaxKind.PercentEqualsToken: - return [true, tstl.SyntaxKind.ModuloOperator]; - case ts.SyntaxKind.AsteriskEqualsToken: - return [true, tstl.SyntaxKind.MultiplicationOperator]; - case ts.SyntaxKind.AmpersandEqualsToken: - return [true, tstl.SyntaxKind.BitwiseAndOperator]; - case ts.SyntaxKind.AsteriskAsteriskEqualsToken: - return [true, tstl.SyntaxKind.PowerOperator]; - case ts.SyntaxKind.LessThanLessThanEqualsToken: - return [true, tstl.SyntaxKind.BitwiseLeftShiftOperator]; - case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: - return [true, tstl.SyntaxKind.BitwiseRightShiftOperator]; - case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - return [true, tstl.SyntaxKind.BitwiseArithmeticRightShift]; - } - - return [false, undefined]; - } - - public static isExpressionStatement(node: ts.Expression): boolean { - return node.parent === undefined || ts.isExpressionStatement(node.parent) || ts.isForStatement(node.parent); - } - - public static isInGlobalScope(node: ts.FunctionDeclaration): boolean { - let parent = node.parent; - while (parent !== undefined) { - if (ts.isBlock(parent)) { - return false; - } - parent = parent.parent; - } - return true; - } - - // Returns true for expressions that may have effects when evaluated - public static isExpressionWithEvaluationEffect(node: ts.Expression): boolean { - return !(ts.isLiteralExpression(node) || ts.isIdentifier(node) || node.kind === ts.SyntaxKind.ThisKeyword); - } - - // If expression is property/element access with possible effects from being evaluated, returns true along with the - // separated object and index expressions. - public static isAccessExpressionWithEvaluationEffects(node: ts.Expression, checker: ts.TypeChecker): - [boolean, ts.Expression, ts.Expression] { - if (ts.isElementAccessExpression(node) && - (TSHelper.isExpressionWithEvaluationEffect(node.expression) - || TSHelper.isExpressionWithEvaluationEffect(node.argumentExpression))) { - const type = checker.getTypeAtLocation(node.expression); - if (TSHelper.isArrayType(type, checker)) { - // Offset arrays by one - const oneLit = ts.createNumericLiteral("1"); - const exp = ts.createParen(node.argumentExpression); - const addExp = ts.createBinary(exp, ts.SyntaxKind.PlusToken, oneLit); - return [true, node.expression, addExp]; - } else { - return [true, node.expression, node.argumentExpression]; - } - } else if (ts.isPropertyAccessExpression(node) && TSHelper.isExpressionWithEvaluationEffect(node.expression)) { - return [true, node.expression, ts.createStringLiteral(node.name.text)]; - } - return [false, undefined, undefined]; - } - - public static isDefaultArrayCallMethodName(methodName: string): boolean { - return defaultArrayCallMethodNames.has(methodName); - } - - public static getExplicitThisParameter(signatureDeclaration: ts.SignatureDeclaration): ts.ParameterDeclaration { - return signatureDeclaration.parameters.find( - param => ts.isIdentifier(param.name) && param.name.originalKeywordKind === ts.SyntaxKind.ThisKeyword); - } - - public static findInClassOrAncestor( - classDeclaration: ts.ClassLikeDeclarationBase, - callback: (classDeclaration: ts.ClassLikeDeclarationBase) => boolean, - checker: ts.TypeChecker - ): ts.ClassLikeDeclarationBase - { - if (callback(classDeclaration)) { - return classDeclaration; - } - - const extendsType = TSHelper.getExtendedType(classDeclaration, checker); - if (!extendsType) { - return undefined; - } - - const symbol = extendsType.getSymbol(); - const declaration = symbol.getDeclarations().find(ts.isClassLike); - if (!declaration) { - return undefined; - } - - return TSHelper.findInClassOrAncestor(declaration, callback, checker); - } - - public static hasSetAccessorInClassOrAncestor( - classDeclaration: ts.ClassLikeDeclarationBase, - isStatic: boolean, - checker: ts.TypeChecker - ): boolean - { - return TSHelper.findInClassOrAncestor( - classDeclaration, - c => c.members.some(m => ts.isSetAccessor(m) && TSHelper.isStatic(m) === isStatic), - checker - ) !== undefined; - } - - public static hasGetAccessorInClassOrAncestor( - classDeclaration: ts.ClassLikeDeclarationBase, - isStatic: boolean, - checker: ts.TypeChecker - ): boolean - { - return TSHelper.findInClassOrAncestor( - classDeclaration, - c => c.members.some(m => ts.isGetAccessor(m) && TSHelper.isStatic(m) === isStatic), - checker - ) !== undefined; - } - - public static getPropertyName(propertyName: ts.PropertyName): string | number | undefined { - if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName) || ts.isNumericLiteral(propertyName)) { - return propertyName.text; - } else { - return undefined; // TODO: how to handle computed property names? - } - } - - public static isSamePropertyName(a: ts.PropertyName, b: ts.PropertyName): boolean { - const aName = TSHelper.getPropertyName(a); - const bName = TSHelper.getPropertyName(b); - return aName !== undefined && aName === bName; - } - - public static isGetAccessorOverride( - element: ts.ClassElement, - classDeclaration: ts.ClassLikeDeclarationBase, - checker: ts.TypeChecker - ): element is ts.GetAccessorDeclaration - { - if (!ts.isGetAccessor(element) || TSHelper.isStatic(element)) { - return false; - } - - const hasInitializedField = (e: ts.ClassElement) => - ts.isPropertyDeclaration(e) - && e.initializer - && TSHelper.isSamePropertyName(e.name, element.name); - - return TSHelper.findInClassOrAncestor( - classDeclaration, - c => c.members.some(hasInitializedField), - checker - ) !== undefined; - } - - public static inferAssignedType(expression: ts.Expression, checker: ts.TypeChecker): ts.Type { - if (ts.isParenthesizedExpression(expression.parent)) { - // Ignore expressions wrapped in parenthesis - return this.inferAssignedType(expression.parent, checker); - - } else if (ts.isCallExpression(expression.parent)) { - // Expression being passed as argument to a function - let i = expression.parent.arguments.indexOf(expression); - if (i >= 0) { - const parentSignature = checker.getResolvedSignature(expression.parent); - const parentSignatureDeclaration = parentSignature.getDeclaration(); - if (parentSignatureDeclaration) { - if (this.getExplicitThisParameter(parentSignatureDeclaration)) { - ++i; - } - return checker.getTypeAtLocation(parentSignatureDeclaration.parameters[i]); - } - } - - } else if (ts.isReturnStatement(expression.parent)) { - // Expression being returned from a function - return this.getContainingFunctionReturnType(expression.parent, checker); - - } else if (ts.isPropertyDeclaration(expression.parent)) { - // Expression being assigned to a class property - return checker.getTypeAtLocation(expression.parent); - - } else if (ts.isPropertyAssignment(expression.parent)) { - // Expression being assigned to an object literal property - const objType = this.inferAssignedType(expression.parent.parent, checker); - const property = objType.getProperty(expression.parent.name.getText()); - if (!property) { - return objType.getStringIndexType(); - } else { - return checker.getTypeAtLocation(property.valueDeclaration); - } - - } else if (ts.isArrayLiteralExpression(expression.parent)) { - // Expression in an array literal - const arrayType = this.inferAssignedType(expression.parent, checker); - if (ts.isTupleTypeNode(checker.typeToTypeNode(arrayType))) { - // Tuples - const i = expression.parent.elements.indexOf(expression); - const elementType = (arrayType as ts.TypeReference).typeArguments[i]; - return elementType; - } else { - // Standard arrays - return arrayType.getNumberIndexType(); - } - - } else if (ts.isVariableDeclaration(expression.parent)) { - // Expression assigned to declaration - return checker.getTypeAtLocation(expression.parent.name); - - } else if (ts.isBinaryExpression(expression.parent) - && expression.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) - { - // Expression assigned to variable - return checker.getTypeAtLocation(expression.parent.left); - } - - return checker.getTypeAtLocation(expression); - } - - public static getSignatureDeclarations( - signatures: ReadonlyArray, - checker: ts.TypeChecker - ): ts.SignatureDeclaration[] - { - const signatureDeclarations: ts.SignatureDeclaration[] = []; - for (const signature of signatures) { - const signatureDeclaration = signature.getDeclaration(); - if ((ts.isFunctionExpression(signatureDeclaration) || ts.isArrowFunction(signatureDeclaration)) - && !TSHelper.getExplicitThisParameter(signatureDeclaration)) - { - // Infer type of function expressions/arrow functions - const inferredType = TSHelper.inferAssignedType(signatureDeclaration, checker); - if (inferredType) { - const inferredSignatures = inferredType.getCallSignatures(); - if (inferredSignatures.length > 0) { - signatureDeclarations.push(...inferredSignatures.map(s => s.getDeclaration())); - continue; - } - } - } - signatureDeclarations.push(signatureDeclaration); - } - return signatureDeclarations; - } - - public static getDeclarationContextType( - signatureDeclaration: ts.SignatureDeclaration, - checker: ts.TypeChecker - ): ContextType - { - const thisParameter = TSHelper.getExplicitThisParameter(signatureDeclaration); - if (thisParameter) { - // Explicit 'this' - return thisParameter.type && thisParameter.type.kind === ts.SyntaxKind.VoidKeyword - ? ContextType.Void - : ContextType.NonVoid; - } - if (ts.isMethodDeclaration(signatureDeclaration) || ts.isMethodSignature(signatureDeclaration)) { - // Method - return ContextType.NonVoid; - } - if (ts.isPropertySignature(signatureDeclaration.parent) - || ts.isPropertyDeclaration(signatureDeclaration.parent) - || ts.isPropertyAssignment(signatureDeclaration.parent)) { - // Lambda property - return ContextType.NonVoid; - } - if (ts.isBinaryExpression(signatureDeclaration.parent)) { - // Function expression: check type being assigned to - return TSHelper.getFunctionContextType( - checker.getTypeAtLocation(signatureDeclaration.parent.left), checker); - } - return ContextType.Void; - } - - public static reduceContextTypes(contexts: ContextType[]): ContextType { - const reducer = (a: ContextType, b: ContextType) => { - if (a === ContextType.None) { - return b; - } else if (b === ContextType.None) { - return a; - } else if (a !== b) { - return ContextType.Mixed; - } else { - return a; - } - }; - return contexts.reduce(reducer, ContextType.None); - } - - public static getFunctionContextType(type: ts.Type, checker: ts.TypeChecker): ContextType { - if (type.isTypeParameter()) { - type = type.getConstraint() || type; - } - - if (type.isUnion()) { - return TSHelper.reduceContextTypes(type.types.map(t => TSHelper.getFunctionContextType(t, checker))); - } - - const signatures = checker.getSignaturesOfType(type, ts.SignatureKind.Call); - if (signatures.length === 0) { - return ContextType.None; - } - const signatureDeclarations = TSHelper.getSignatureDeclarations(signatures, checker); - return TSHelper.reduceContextTypes( - signatureDeclarations.map(s => TSHelper.getDeclarationContextType(s, checker))); - } - - public static isDefaultArrayPropertyName(methodName: string): boolean { - return defaultArrayPropertyNames.has(methodName); - } - - public static escapeString(text: string): string { - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String - const escapeSequences: Array<[RegExp, string]> = [ - [/[\\]/g, "\\\\"], - [/[\']/g, "\\\'"], - [/[\`]/g, "\\\`"], - [/[\"]/g, "\\\""], - [/[\n]/g, "\\n"], - [/[\r]/g, "\\r"], - [/[\v]/g, "\\v"], - [/[\t]/g, "\\t"], - [/[\b]/g, "\\b"], - [/[\f]/g, "\\f"], - [/[\0]/g, "\\0"], - ]; - - if (text.length > 0) { - for (const [regex, replacement] of escapeSequences) { - text = text.replace(regex, replacement); - } - } - return text; - } - - public static isValidLuaIdentifier(str: string): boolean { - const match = str.match(/[a-zA-Z_][a-zA-Z0-9_]*/); - return match && match[0] === str; - } - - public static isFalsible(type: ts.Type, strictNullChecks: boolean): boolean { - const falsibleFlags = ts.TypeFlags.Boolean - | ts.TypeFlags.BooleanLiteral - | ts.TypeFlags.Undefined - | ts.TypeFlags.Null - | ts.TypeFlags.Never - | ts.TypeFlags.Void - | ts.TypeFlags.Any; - - if (type.flags & falsibleFlags) { - return true; - } else if (!strictNullChecks && !type.isLiteral()) { - return true; - } else if (type.isUnion()) { - for (const subType of type.types) { - if (TSHelper.isFalsible(subType, strictNullChecks)) { - return true; - } - } - } - - return false; - } - - public static getFirstDeclaration(symbol: ts.Symbol, sourceFile?: ts.SourceFile): ts.Declaration | undefined { - let declarations = symbol.getDeclarations(); - if (!declarations) { - return undefined; - } - if (sourceFile) { - declarations = declarations.filter(d => this.findFirstNodeAbove(d, ts.isSourceFile) === sourceFile); - } - return declarations.length > 0 - ? declarations.reduce((p, c) => p.pos < c.pos ? p : c) - : undefined; - } - - public static isFirstDeclaration(node: ts.VariableDeclaration, checker: ts.TypeChecker): boolean { - const symbol = checker.getSymbolAtLocation(node.name); - if (!symbol) { - return false; - } - const firstDeclaration = this.getFirstDeclaration(symbol); - return firstDeclaration === node; - } - - public static isEnumMember(enumDeclaration: ts.EnumDeclaration, value: ts.Expression): [boolean, ts.PropertyName] { - if (ts.isIdentifier(value)) { - const enumMember = enumDeclaration.members.find(m => ts.isIdentifier(m.name) && m.name.text === value.text); - if (enumMember !== undefined) { - if (enumMember.initializer && ts.isIdentifier(enumMember.initializer)) { - return this.isEnumMember(enumDeclaration, enumMember.initializer); - } else { - return [true, enumMember.name]; - } - } else { - return [false, undefined]; - } - } else { - return [false, undefined]; - } - } -} diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts deleted file mode 100644 index 53a00cce7..000000000 --- a/src/TSTLErrors.ts +++ /dev/null @@ -1,147 +0,0 @@ -import * as ts from "typescript"; - -import {TranspileError} from "./TranspileError"; -import {TSHelper as tsHelper} from "./TSHelper"; - -export class TSTLErrors { - public static CouldNotFindEnumMember = (enumDeclaration: ts.EnumDeclaration, enumMember: string, node: ts.Node) => - new TranspileError(`Could not find ${enumMember} in ${enumDeclaration.name.text}`, node); - - public static DefaultImportsNotSupported = (node: ts.Node) => - new TranspileError(`Default Imports are not supported, please use named imports instead!`, node); - - public static ForbiddenEllipsisDestruction = (node: ts.Node) => - new TranspileError(`Ellipsis destruction is not allowed.`, node); - - public static ForbiddenForIn = (node: ts.Node) => - new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node); - - public static HeterogeneousEnum = (node: ts.Node) => new TranspileError( - `Invalid heterogeneous enum. Enums should either specify no member values, ` + - `or specify values (of the same type) for all members.`, - node); - - public static InvalidDecoratorArgumentNumber = (name: string, got: number, expected: number, node: ts.Node) => - new TranspileError(`${name} expects ${expected} argument(s) but got ${got}.`, node); - - public static InvalidExtensionMetaExtension = (node: ts.Node) => - new TranspileError(`Cannot use both '@extension' and '@metaExtension' decorators on the same class.`, node); - - public static InvalidNewExpressionOnExtension = (node: ts.Node) => - new TranspileError(`Cannot construct classes with decorator '@extension' or '@metaExtension'.`, node); - - public static InvalidExtendsExtension = (node: ts.Node) => - new TranspileError(`Cannot extend classes with decorator '@extension' or '@metaExtension'.`, node); - - public static InvalidExportsExtension = (node: ts.Node) => - new TranspileError(`Cannot export classes with decorator '@extension' or '@metaExtension'.`, node); - - public static InvalidInstanceOfExtension = (node: ts.Node) => - new TranspileError(`Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'.`, node); - - public static InvalidJsonFileContent = (node: ts.Node) => - new TranspileError("Invalid JSON file content", node); - - public static InvalidPropertyCall = (node: ts.Node) => - new TranspileError(`Tried to transpile a non-property call as property call.`, node); - - public static InvalidElementCall = (node: ts.Node) => - new TranspileError(`Tried to transpile a non-element call as an element call.`, node); - - public static InvalidThrowExpression = (node: ts.Node) => - new TranspileError(`Invalid throw expression, only strings can be thrown.`, node); - - public static KeywordIdentifier = (node: ts.Identifier) => - new TranspileError(`Cannot use Lua keyword ${node.escapedText} as identifier.`, node); - - public static MissingClassName = (node: ts.Node) => - new TranspileError(`Class declarations must have a name.`, node); - - public static MissingMetaExtension = (node: ts.Node) => - new TranspileError(`@metaExtension requires the extension of the metatable class.`, node); - - public static UnsupportedDefaultExport = (node: ts.Node) => - new TranspileError(`Default exports are not supported.`, node); - - public static UnsupportedImportType = (node: ts.Node) => - new TranspileError(`Unsupported import type.`, node); - - public static UnsupportedKind = (description: string, kind: ts.SyntaxKind, node: ts.Node) => - { - const kindName = tsHelper.enumName(kind, ts.SyntaxKind); - return new TranspileError(`Unsupported ${description} kind: ${kindName}`, node); - }; - - public static UnsupportedProperty = (parentName: string, property: string, node: ts.Node) => - new TranspileError(`Unsupported property on ${parentName}: ${property}`, node); - - public static UnsupportedForTarget = (functionality: string, version: string, node: ts.Node) => - new TranspileError(`${functionality} is/are not supported for target Lua ${version}.`, node); - - public static UnsupportedFunctionConversion = (node: ts.Node, name?: string) => { - if (name) { - return new TranspileError( - `Unsupported conversion from method to function "${name}". ` + - `To fix, wrap the method in an arrow function.`, - node); - } else { - return new TranspileError( - `Unsupported conversion from method to function. ` + - `To fix, wrap the method in an arrow function.`, - node); - } - }; - - public static UnsupportedMethodConversion = (node: ts.Node, name?: string) => { - if (name) { - return new TranspileError( - `Unsupported conversion from function to method "${name}". ` + - `To fix, wrap the function in an arrow function or declare the function with` + - ` an explicit 'this' parameter.`, - node); - } else { - return new TranspileError( - `Unsupported conversion from function to method. ` + - `To fix, wrap the function in an arrow function or declare the function with` + - ` an explicit 'this' parameter.`, - node); - } - }; - - public static UnsupportedOverloadAssignment = (node: ts.Node, name?: string) => { - if (name) { - return new TranspileError( - `Unsupported assignment of mixed function/method overload to "${name}". ` + - `Overloads should either be all functions or all methods, but not both.`, - node); - } else { - return new TranspileError( - `Unsupported assignment of mixed function/method overload. ` + - `Overloads should either be all functions or all methods, but not both.`, - node); - } - }; - - public static UnsupportedNonDestructuringLuaIterator = (node: ts.Node) => { - return new TranspileError( - "Unsupported use of lua iterator with TupleReturn decorator in for...of statement. " + - "You must use a destructuring statement to catch results from a lua iterator with " + - "the TupleReturn decorator.", - node); - }; - - public static UnresolvableRequirePath = (node: ts.Node, reason: string, path?: string) => { - return new TranspileError( - `${reason}. ` + - `TypeScript path: ${path}.`, - node); - }; - - public static ReferencedBeforeDeclaration = (node: ts.Identifier) => { - return new TranspileError( - `Identifier "${node.text}" was referenced before it was declared. The declaration ` + - "must be moved before the identifier's use, or hoisting must be enabled.", - node - ); - } -} diff --git a/src/TranspileError.ts b/src/TranspileError.ts deleted file mode 100644 index c05b7a703..000000000 --- a/src/TranspileError.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as ts from "typescript"; - -export class TranspileError extends Error { - public node: ts.Node; - constructor(message: string, node: ts.Node) { - super(message); - this.node = node; - } -} diff --git a/src/cli/diagnostics.ts b/src/cli/diagnostics.ts new file mode 100644 index 000000000..963713d2b --- /dev/null +++ b/src/cli/diagnostics.ts @@ -0,0 +1,74 @@ +import * as ts from "typescript"; +import { createSerialDiagnosticFactory, createDiagnosticFactoryWithCode } from "../utils"; + +export const tstlOptionsAreMovingToTheTstlObject = createSerialDiagnosticFactory((tstl: Record) => ({ + category: ts.DiagnosticCategory.Warning, + messageText: + 'TSTL options are moving to the "tstl" object. Adjust your tsconfig to look like\n' + + `"tstl": ${JSON.stringify(tstl, undefined, 4)}`, +})); + +export const watchErrorSummary = (errorCount: number): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Message, + code: errorCount === 1 ? 6193 : 6194, + messageText: + errorCount === 1 + ? "Found 1 error. Watching for file changes." + : `Found ${errorCount} errors. Watching for file changes.`, +}); + +const createCommandLineError = (code: number, getMessage: (...args: TArgs) => string) => + createDiagnosticFactoryWithCode(code, (...args: TArgs) => ({ messageText: getMessage(...args) })); + +export const unknownCompilerOption = createCommandLineError( + 5023, + (name: string) => `Unknown compiler option '${name}'.` +); + +export const compilerOptionRequiresAValueOfType = createCommandLineError( + 5024, + (name: string, type: string) => `Compiler option '${name}' requires a value of type ${type}.` +); + +export const compilerOptionCouldNotParseJson = createCommandLineError( + 5025, + (name: string, error: string) => `Compiler option '${name}' failed to parse the given JSON value: '${error}'.` +); + +export const optionProjectCannotBeMixedWithSourceFilesOnACommandLine = createCommandLineError( + 5042, + () => "Option 'project' cannot be mixed with source files on a command line." +); + +export const cannotFindATsconfigJsonAtTheSpecifiedDirectory = createCommandLineError( + 5057, + (dir: string) => `Cannot find a tsconfig.json file at the specified directory: '${dir}'.` +); + +export const theSpecifiedPathDoesNotExist = createCommandLineError( + 5058, + (dir: string) => `The specified path does not exist: '${dir}'.` +); + +export const compilerOptionExpectsAnArgument = createCommandLineError( + 6044, + (name: string) => `Compiler option '${name}' expects an argument.` +); + +export const argumentForOptionMustBe = createCommandLineError( + 6046, + (name: string, values: string) => `Argument for '${name}' option must be: ${values}.` +); + +export const optionCanOnlyBeSpecifiedInTsconfigJsonFile = createCommandLineError( + 6064, + (name: string) => `Option '${name}' can only be specified in 'tsconfig.json' file.` +); + +export const optionBuildMustBeFirstCommandLineArgument = createCommandLineError( + 6369, + () => "Option '--build' must be the first command line argument." +); diff --git a/src/cli/information.ts b/src/cli/information.ts new file mode 100644 index 000000000..4c4b3ce1a --- /dev/null +++ b/src/cli/information.ts @@ -0,0 +1,32 @@ +import { optionDeclarations } from "./parse"; + +export const { version } = require("../../package.json"); +export const versionString = `Version ${version}`; + +const helpString = ` +Syntax: tstl [options] [files...] + +Examples: tstl path/to/file.ts [...] + tstl -p path/to/tsconfig.json + +In addition to the options listed below you can also pass options +for the typescript compiler (For a list of options use tsc -h). +Some tsc options might have no effect. +`.trim(); + +export function getHelpString(): string { + let result = helpString + "\n\n"; + + result += "Options:\n"; + for (const option of optionDeclarations) { + const aliasStrings = (option.aliases ?? []).map(a => "-" + a); + const optionString = [...aliasStrings, "--" + option.name].join("|"); + + const valuesHint = option.type === "enum" ? option.choices.join("|") : option.type; + const spacing = " ".repeat(Math.max(1, 45 - optionString.length - valuesHint.length)); + + result += `\n ${optionString} <${valuesHint}>${spacing}${option.description}\n`; + } + + return result; +} diff --git a/src/cli/parse.ts b/src/cli/parse.ts new file mode 100644 index 000000000..ca4f039ad --- /dev/null +++ b/src/cli/parse.ts @@ -0,0 +1,291 @@ +import * as ts from "typescript"; +import { BuildMode, CompilerOptions, LuaLibImportKind, LuaTarget } from "../CompilerOptions"; +import * as cliDiagnostics from "./diagnostics"; + +export interface ParsedCommandLine extends ts.ParsedCommandLine { + options: CompilerOptions; +} + +interface CommandLineOptionBase { + name: string; + aliases?: string[]; + description: string; +} + +interface CommandLineOptionOfEnum extends CommandLineOptionBase { + type: "enum"; + choices: string[]; +} + +interface CommandLineOptionOfPrimitive extends CommandLineOptionBase { + type: "boolean" | "string" | "json-array-of-objects" | "array"; +} + +type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfPrimitive; + +export const optionDeclarations: CommandLineOption[] = [ + { + name: "buildMode", + description: "'default' or 'library'. Compiling as library will not resolve external dependencies.", + type: "enum", + choices: Object.values(BuildMode), + }, + { + name: "extension", + description: 'File extension for the resulting Lua files. Defaults to ".lua"', + type: "string", + }, + { + name: "luaBundle", + description: "The name of the lua file to bundle output lua to. Requires luaBundleEntry.", + type: "string", + }, + { + name: "luaBundleEntry", + description: "The entry *.ts file that will be executed when entering the luaBundle. Requires luaBundle.", + type: "string", + }, + { + name: "luaLibImport", + description: "Specifies how js standard features missing in lua are imported.", + type: "enum", + choices: Object.values(LuaLibImportKind), + }, + { + name: "luaTarget", + aliases: ["lt"], + description: "Specify Lua target version.", + type: "enum", + choices: Object.values(LuaTarget), + }, + { + name: "noImplicitGlobalVariables", + description: + 'Specify to prevent implicitly turning "normal" variants into global variables in the transpiled output.', + type: "boolean", + }, + { + name: "noImplicitSelf", + description: 'If "this" is implicitly considered an any type, do not generate a self parameter.', + type: "boolean", + }, + { + name: "noHeader", + description: "Specify if a header will be added to compiled files.", + type: "boolean", + }, + { + name: "sourceMapTraceback", + description: "Applies the source map to show source TS files and lines in error tracebacks.", + type: "boolean", + }, + { + name: "luaPlugins", + description: "List of TypeScriptToLua plugins.", + type: "json-array-of-objects", + }, + { + name: "tstlVerbose", + description: "Provide verbose output useful for diagnosing problems.", + type: "boolean", + }, + { + name: "noResolvePaths", + description: "An array of paths that tstl should not resolve and keep as-is.", + type: "array", + }, + { + name: "lua51AllowTryCatchInAsyncAwait", + description: "Always allow try/catch in async/await functions for Lua 5.1.", + type: "boolean", + }, + { + name: "measurePerformance", + description: "Measure performance of the tstl compiler.", + type: "boolean", + }, +]; + +export function updateParsedConfigFile(parsedConfigFile: ts.ParsedCommandLine): ParsedCommandLine { + let hasRootLevelOptions = false; + for (const [name, rawValue] of Object.entries(parsedConfigFile.raw)) { + const option = optionDeclarations.find(option => option.name === name); + if (!option) continue; + + if (parsedConfigFile.raw.tstl === undefined) parsedConfigFile.raw.tstl = {}; + parsedConfigFile.raw.tstl[name] = rawValue; + hasRootLevelOptions = true; + } + + if (parsedConfigFile.raw.tstl) { + if (hasRootLevelOptions) { + parsedConfigFile.errors.push(cliDiagnostics.tstlOptionsAreMovingToTheTstlObject(parsedConfigFile.raw.tstl)); + } + + for (const [name, rawValue] of Object.entries(parsedConfigFile.raw.tstl)) { + const option = optionDeclarations.find(option => option.name === name); + if (!option) { + parsedConfigFile.errors.push(cliDiagnostics.unknownCompilerOption(name)); + continue; + } + + const { error, value } = readValue(option, rawValue, OptionSource.TsConfig); + if (error) parsedConfigFile.errors.push(error); + if (parsedConfigFile.options[name] === undefined) parsedConfigFile.options[name] = value; + } + } + + return parsedConfigFile; +} + +export function parseCommandLine(args: string[]): ParsedCommandLine { + return updateParsedCommandLine(ts.parseCommandLine(args), args); +} + +function updateParsedCommandLine(parsedCommandLine: ts.ParsedCommandLine, args: string[]): ParsedCommandLine { + for (let i = 0; i < args.length; i++) { + if (!args[i].startsWith("-")) continue; + + const isShorthand = !args[i].startsWith("--"); + const argumentName = args[i].substring(isShorthand ? 1 : 2); + const option = optionDeclarations.find(option => { + if (option.name.toLowerCase() === argumentName.toLowerCase()) return true; + if (isShorthand && option.aliases) { + return option.aliases.some(a => a.toLowerCase() === argumentName.toLowerCase()); + } + + return false; + }); + + if (option) { + // Ignore errors caused by tstl specific compiler options + parsedCommandLine.errors = parsedCommandLine.errors.filter( + // TS5023: Unknown compiler option '{0}'. + // TS5025: Unknown compiler option '{0}'. Did you mean '{1}'? + e => !((e.code === 5023 || e.code === 5025) && String(e.messageText).includes(`'${args[i]}'.`)) + ); + + const { error, value, consumed } = readCommandLineArgument(option, args[i + 1]); + if (error) parsedCommandLine.errors.push(error); + parsedCommandLine.options[option.name] = value; + if (consumed) { + // Values of custom options are parsed as a file name, exclude them + parsedCommandLine.fileNames = parsedCommandLine.fileNames.filter(f => f !== args[i + 1]); + i += 1; + } + } + } + + return parsedCommandLine; +} + +interface CommandLineArgument extends ReadValueResult { + consumed: boolean; +} + +function readCommandLineArgument(option: CommandLineOption, value: any): CommandLineArgument { + if (option.type === "boolean") { + if (value === "true" || value === "false") { + value = value === "true"; + } else { + // Set boolean arguments without supplied value to true + return { value: true, consumed: false }; + } + } + + if (value === undefined) { + return { + error: cliDiagnostics.compilerOptionExpectsAnArgument(option.name), + value: undefined, + consumed: false, + }; + } + + return { ...readValue(option, value, OptionSource.CommandLine), consumed: true }; +} + +enum OptionSource { + CommandLine, + TsConfig, +} + +interface ReadValueResult { + error?: ts.Diagnostic; + value: any; +} + +function readValue(option: CommandLineOption, value: unknown, source: OptionSource): ReadValueResult { + if (value === null) return { value }; + + switch (option.type) { + case "boolean": + case "string": { + if (typeof value !== option.type) { + return { + value: undefined, + error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, option.type), + }; + } + + return { value }; + } + case "array": + case "json-array-of-objects": { + const isInvalidNonCliValue = source === OptionSource.TsConfig && !Array.isArray(value); + const isInvalidCliValue = source === OptionSource.CommandLine && typeof value !== "string"; + + if (isInvalidNonCliValue || isInvalidCliValue) { + return { + value: undefined, + error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, option.type), + }; + } + + const shouldParseValue = source === OptionSource.CommandLine && typeof value === "string"; + if (!shouldParseValue) return { value }; + + if (option.type === "array") { + const array = value.split(","); + return { value: array }; + } + + try { + const objects = JSON.parse(value); + if (!Array.isArray(objects)) { + return { + value: undefined, + error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, option.type), + }; + } + + return { value: objects }; + } catch (e) { + if (!(e instanceof SyntaxError)) throw e; + + return { + value: undefined, + error: cliDiagnostics.compilerOptionCouldNotParseJson(option.name, e.message), + }; + } + } + case "enum": { + if (typeof value !== "string") { + return { + value: undefined, + error: cliDiagnostics.compilerOptionRequiresAValueOfType(option.name, "string"), + }; + } + + const enumValue = option.choices.find(c => c.toLowerCase() === value.toLowerCase()); + if (enumValue === undefined) { + const optionChoices = option.choices.join(", "); + return { + value: undefined, + error: cliDiagnostics.argumentForOptionMustBe(`--${option.name}`, optionChoices), + }; + } + + return { value: enumValue }; + } + } +} diff --git a/src/cli/report.ts b/src/cli/report.ts new file mode 100644 index 000000000..19fd02d1e --- /dev/null +++ b/src/cli/report.ts @@ -0,0 +1,9 @@ +import * as ts from "typescript"; + +export const prepareDiagnosticForFormatting = (diagnostic: ts.Diagnostic) => + diagnostic.source === "typescript-to-lua" ? { ...diagnostic, code: "TL" as any } : diagnostic; + +export function createDiagnosticReporter(pretty: boolean, system = ts.sys): ts.DiagnosticReporter { + const reporter = ts.createDiagnosticReporter(system, pretty); + return diagnostic => reporter(prepareDiagnosticForFormatting(diagnostic)); +} diff --git a/src/cli/tsconfig.ts b/src/cli/tsconfig.ts new file mode 100644 index 000000000..33e469368 --- /dev/null +++ b/src/cli/tsconfig.ts @@ -0,0 +1,148 @@ +import * as path from "path"; +import * as ts from "typescript"; +import { CompilerOptions, TypeScriptToLuaOptions } from "../CompilerOptions"; +import { normalizeSlashes } from "../utils"; +import * as cliDiagnostics from "./diagnostics"; +import { ParsedCommandLine, updateParsedConfigFile } from "./parse"; + +export function locateConfigFile(commandLine: ParsedCommandLine): ts.Diagnostic | string | undefined { + const { project } = commandLine.options; + if (!project) { + if (commandLine.fileNames.length > 0) { + return undefined; + } + + const searchPath = normalizeSlashes(process.cwd()); + return ts.findConfigFile(searchPath, ts.sys.fileExists); + } + + if (commandLine.fileNames.length !== 0) { + return cliDiagnostics.optionProjectCannotBeMixedWithSourceFilesOnACommandLine(); + } + + // TODO: Unlike tsc, this resolves `.` to absolute path + const fileOrDirectory = normalizeSlashes(path.resolve(process.cwd(), project)); + if (ts.sys.directoryExists(fileOrDirectory)) { + const configFileName = path.posix.join(fileOrDirectory, "tsconfig.json"); + if (ts.sys.fileExists(configFileName)) { + return configFileName; + } else { + return cliDiagnostics.cannotFindATsconfigJsonAtTheSpecifiedDirectory(project); + } + } else if (ts.sys.fileExists(fileOrDirectory)) { + return fileOrDirectory; + } else { + return cliDiagnostics.theSpecifiedPathDoesNotExist(project); + } +} + +export function parseConfigFileWithSystem( + configFileName: string, + commandLineOptions?: CompilerOptions, + system = ts.sys +): ParsedCommandLine { + const configRootDir = path.dirname(configFileName); + const parsedConfigFile = ts.parseJsonSourceFileConfigFileContent( + ts.readJsonConfigFile(configFileName, system.readFile), + system, + configRootDir, + commandLineOptions, + configFileName + ); + + const cycleCache = new Set(); + const extendedTstlOptions = getExtendedTstlOptions(configFileName, configRootDir, cycleCache, system); + + parsedConfigFile.raw.tstl = Object.assign(extendedTstlOptions, parsedConfigFile.raw.tstl ?? {}); + + return updateParsedConfigFile(parsedConfigFile); +} + +function resolveNpmModuleConfig( + moduleName: string, + configRootDir: string, + host: ts.ModuleResolutionHost +): string | undefined { + const resolved = ts.nodeNextJsonConfigResolver(moduleName, path.join(configRootDir, "tsconfig.json"), host); + if (resolved.resolvedModule) { + return resolved.resolvedModule.resolvedFileName; + } +} + +function getExtendedTstlOptions( + configFilePath: string, + configRootDir: string, + cycleCache: Set, + system: ts.System +): TypeScriptToLuaOptions { + const absolutePath = ts.pathIsAbsolute(configFilePath) + ? configFilePath + : ts.pathIsRelative(configFilePath) + ? path.resolve(configRootDir, configFilePath) + : resolveNpmModuleConfig(configFilePath, configRootDir, system); // if a path is neither relative nor absolute, it is probably a npm module + + if (!absolutePath) { + return {}; + } + + const newConfigRoot = path.dirname(absolutePath); + + if (cycleCache.has(absolutePath)) { + return {}; + } + + cycleCache.add(absolutePath); + const fileContent = system.readFile(absolutePath); + const options = {}; + + if (fileContent) { + const { config: parsedConfig } = ts.parseConfigFileTextToJson(configFilePath, fileContent) as { + config?: { + extends?: string | string[]; + tstl?: TypeScriptToLuaOptions; + }; + }; + + if (!parsedConfig) { + return {}; + } + + if (parsedConfig.extends) { + if (Array.isArray(parsedConfig.extends)) { + for (const extendedConfigFile of parsedConfig.extends) { + Object.assign( + options, + getExtendedTstlOptions(extendedConfigFile, newConfigRoot, cycleCache, system) + ); + } + } else { + Object.assign(options, getExtendedTstlOptions(parsedConfig.extends, newConfigRoot, cycleCache, system)); + } + } + + if (parsedConfig.tstl) { + Object.assign(options, parsedConfig.tstl); + } + } + + return options; +} + +export function createConfigFileUpdater( + optionsToExtend: CompilerOptions +): (options: ts.CompilerOptions) => ts.Diagnostic[] { + const configFileMap = new WeakMap(); + return options => { + const { configFile, configFilePath } = options; + if (!configFile || !configFilePath) return []; + + if (!configFileMap.has(configFile)) { + const parsedConfigFile = parseConfigFileWithSystem(configFilePath, optionsToExtend, ts.sys); + configFileMap.set(configFile, parsedConfigFile); + } + + const parsedConfigFile = configFileMap.get(configFile)!; + Object.assign(options, parsedConfigFile.options); + return parsedConfigFile.errors; + }; +} diff --git a/src/index.ts b/src/index.ts index 7c9dec9ea..2d2e3de54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,11 @@ -export {parseCommandLine} from "./CommandLineParser"; -export {compile, compileFilesWithOptions, transpileString, watchWithOptions} from "./Compiler"; -export {CompilerOptions, LuaLibImportKind, LuaTarget,} from "./CompilerOptions"; -export {LuaLibFeature,} from "./LuaLib"; -export {LuaTranspiler,} from "./LuaTranspiler"; +export { version } from "./cli/information"; +export { parseCommandLine, ParsedCommandLine, updateParsedConfigFile } from "./cli/parse"; +export * from "./cli/report"; +export { parseConfigFileWithSystem } from "./cli/tsconfig"; +export * from "./CompilerOptions"; +export * from "./LuaAST"; +export { LuaLibFeature } from "./LuaLib"; +export * from "./LuaPrinter"; +export * from "./transformation/context"; +export * from "./transpilation"; +export { EmitHost, EmitFile, ProcessedFile } from "./transpilation/utils"; diff --git a/src/lualib-build/plugin.ts b/src/lualib-build/plugin.ts new file mode 100644 index 000000000..79233f3df --- /dev/null +++ b/src/lualib-build/plugin.ts @@ -0,0 +1,188 @@ +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +import * as tstl from ".."; +import * as path from "path"; +import { + getLualibBundleReturn, + LuaLibFeature, + LuaLibModulesInfo, + luaLibModulesInfoFileName, + resolveRecursiveLualibFeatures, +} from "../LuaLib"; +import { EmitHost, ProcessedFile } from "../transpilation/utils"; +import { + isExportAlias, + isExportAssignment, + isExportsReturn, + isExportTableDeclaration, + isImport, + isRequire, +} from "./util"; +import { createDiagnosticFactoryWithCode } from "../utils"; + +export const lualibDiagnostic = createDiagnosticFactoryWithCode(200000, (message: string, file?: ts.SourceFile) => ({ + messageText: message, + file, + start: file && 0, + length: file && 0, +})); + +class LuaLibPlugin implements tstl.Plugin { + // Plugin members + public visitors = { + [ts.SyntaxKind.SourceFile]: this.lualibFileVisitor.bind(this), + }; + + public printer: tstl.Printer = (program, emitHost, fileName, file) => + new LuaLibPrinter(emitHost, program, fileName).print(file); + + public afterPrint(program: ts.Program, options: tstl.CompilerOptions, emitHost: EmitHost, result: ProcessedFile[]) { + void options; + + // Write lualib dependency json + const { result: luaLibModuleInfo, diagnostics } = this.createLuaLibModulesInfo(); + const emitBOM = options.emitBOM ?? false; + emitHost.writeFile( + path.join(tstl.getEmitOutDir(program), luaLibModulesInfoFileName), + JSON.stringify(luaLibModuleInfo, null, 2), + emitBOM + ); + + // Flatten the output folder structure; we do not want to keep the target-specific directories + for (const file of result) { + let outPath = file.fileName; + while (outPath.includes("lualib") && path.basename(path.dirname(outPath)) !== "lualib") { + const upOne = path.join(path.dirname(outPath), "..", path.basename(outPath)); + outPath = path.normalize(upOne); + } + file.fileName = outPath; + } + + // Create map of result files keyed by their 'lualib name' + const exportedLualibFeatures = new Map(result.map(f => [path.basename(f.fileName).split(".")[0], f.code])); + + // Figure out the order required in the bundle by recursively resolving all dependency features + const allFeatures = Object.values(LuaLibFeature) as LuaLibFeature[]; + const luaTarget = options.luaTarget ?? tstl.LuaTarget.Universal; + const orderedFeatures = resolveRecursiveLualibFeatures(allFeatures, luaTarget, emitHost, luaLibModuleInfo); + + // Concatenate lualib files into bundle with exports table and add lualib_bundle.lua to results + let lualibBundle = orderedFeatures.map(f => exportedLualibFeatures.get(LuaLibFeature[f])).join("\n"); + const exports = allFeatures.flatMap(feature => luaLibModuleInfo[feature].exports); + lualibBundle += getLualibBundleReturn(exports); + result.push({ fileName: "lualib_bundle.lua", code: lualibBundle }); + + return diagnostics; + } + + // Internals + protected featureExports: Map> = new Map(); + protected featureDependencies: Map> = new Map(); + + protected lualibFileVisitor(file: ts.SourceFile, context: tstl.TransformationContext): tstl.File { + const featureName = path.basename(file.fileName, ".ts") as tstl.LuaLibFeature; + if (!(featureName in tstl.LuaLibFeature)) { + context.diagnostics.push(lualibDiagnostic(`File is not a lualib feature: ${featureName}`, file)); + } + + // Transpile file as normal with tstl + const fileResult = context.superTransformNode(file)[0] as tstl.File; + + const usedFeatures = new Set(context.usedLuaLibFeatures); + + // Get all imports in file + const importNames = new Set(); + const imports = file.statements.filter(ts.isImportDeclaration); + for (const { importClause, moduleSpecifier } of imports) { + if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) { + for (const { name } of importClause.namedBindings.elements) { + importNames.add(name.text); + } + } + // track lualib imports + if (ts.isStringLiteral(moduleSpecifier)) { + const featureName = path.basename(moduleSpecifier.text, ".ts") as tstl.LuaLibFeature; + if (featureName in tstl.LuaLibFeature) { + usedFeatures.add(featureName); + } + } + } + + const filteredStatements = fileResult.statements + .filter( + s => !isExportTableDeclaration(s) && !isRequire(s) && !isImport(s, importNames) && !isExportsReturn(s) + ) + .map(statement => { + if (isExportAlias(statement)) { + const name = statement.left[0]; + const exportName = statement.right[0].index.value; + if (name.text === exportName) return undefined; // Remove "x = x" statements + return tstl.createAssignmentStatement(name, tstl.createIdentifier(exportName)); + } + return statement; + }) + .filter(statement => statement !== undefined); + + const exportNames = filteredStatements.filter(isExportAssignment).map(s => s.left[0].index.value); + if (!filteredStatements.every(isExportAssignment)) { + // If there are local statements, wrap them in a do ... end with exports outside + const exports = tstl.createVariableDeclarationStatement(exportNames.map(k => tstl.createIdentifier(k))); + // transform export assignments to local assignments + const bodyStatements = filteredStatements.map(s => + isExportAssignment(s) + ? tstl.createAssignmentStatement(tstl.createIdentifier(s.left[0].index.value), s.right[0]) + : s + ); + + fileResult.statements = [exports, tstl.createDoStatement(bodyStatements)]; + } else { + // transform export assignments to local variable declarations + fileResult.statements = filteredStatements.map(s => + tstl.createVariableDeclarationStatement(tstl.createIdentifier(s.left[0].index.value), s.right[0]) + ); + } + + // Save dependency information + this.featureExports.set(featureName, new Set(exportNames)); + if (usedFeatures.size > 0) { + this.featureDependencies.set(featureName, usedFeatures); + } + + return fileResult; + } + + protected createLuaLibModulesInfo(): { result: LuaLibModulesInfo; diagnostics: ts.Diagnostic[] } { + const result: Partial = {}; + const diagnostics: ts.Diagnostic[] = []; + for (const feature of Object.values(tstl.LuaLibFeature)) { + const exports = this.featureExports.get(feature); + if (!exports) { + diagnostics.push(lualibDiagnostic(`Missing file for lualib feature: ${feature}`)); + continue; + } + const dependencies = this.featureDependencies.get(feature); + result[feature] = { + exports: Array.from(exports), + dependencies: dependencies ? Array.from(dependencies) : undefined, + }; + } + return { result: result as LuaLibModulesInfo, diagnostics }; + } +} + +class LuaLibPrinter extends tstl.LuaPrinter { + // Strip all exports during print + public printTableIndexExpression(expression: tstl.TableIndexExpression): SourceNode { + if ( + tstl.isIdentifier(expression.table) && + expression.table.text === "____exports" && + tstl.isStringLiteral(expression.index) + ) { + return super.printExpression(tstl.createIdentifier(expression.index.value)); + } + return super.printTableIndexExpression(expression); + } +} + +const pluginInstance = new LuaLibPlugin(); +export default pluginInstance; diff --git a/src/lualib-build/util.ts b/src/lualib-build/util.ts new file mode 100644 index 000000000..c8ea1e3a8 --- /dev/null +++ b/src/lualib-build/util.ts @@ -0,0 +1,47 @@ +import * as tstl from ".."; + +export function isExportTableDeclaration(node: tstl.Node): node is tstl.VariableDeclarationStatement & { left: [] } { + return tstl.isVariableDeclarationStatement(node) && isExportTable(node.left[0]); +} + +export function isExportTable(node: tstl.Node): node is tstl.Identifier { + return tstl.isIdentifier(node) && node.text === "____exports"; +} + +export type ExportTableIndex = tstl.TableIndexExpression & { index: tstl.StringLiteral }; +export function isExportTableIndex(node: tstl.Node): node is ExportTableIndex { + return tstl.isTableIndexExpression(node) && isExportTable(node.table) && tstl.isStringLiteral(node.index); +} + +export function isExportAlias( + node: tstl.Node +): node is tstl.VariableDeclarationStatement & { right: [ExportTableIndex] } { + return tstl.isVariableDeclarationStatement(node) && node.right !== undefined && isExportTableIndex(node.right[0]); +} + +export type ExportAssignment = tstl.AssignmentStatement & { left: [ExportTableIndex] }; +export function isExportAssignment(node: tstl.Node): node is ExportAssignment { + return tstl.isAssignmentStatement(node) && isExportTableIndex(node.left[0]); +} + +export function isRequire(node: tstl.Node) { + return ( + tstl.isVariableDeclarationStatement(node) && + node.right && + tstl.isCallExpression(node.right[0]) && + tstl.isIdentifier(node.right[0].expression) && + node.right[0].expression.text === "require" + ); +} + +export function isImport(node: tstl.Node, importNames: Set) { + return tstl.isVariableDeclarationStatement(node) && importNames.has(node.left[0].text); +} + +export function isExportsReturn(node: tstl.Node) { + return ( + tstl.isReturnStatement(node) && + tstl.isIdentifier(node.expressions[0]) && + node.expressions[0].text === "____exports" + ); +} diff --git a/src/lualib/5.0/CountVarargs.ts b/src/lualib/5.0/CountVarargs.ts new file mode 100644 index 000000000..34467ebd4 --- /dev/null +++ b/src/lualib/5.0/CountVarargs.ts @@ -0,0 +1,7 @@ +/** @noSelfInFile */ + +export function __TS__CountVarargs(...args: T[]): number { + // select() is not available in Lua 5.0. In this version, the arg table + // includes trailing nils. + return args.length; +} diff --git a/src/lualib/5.0/Match.ts b/src/lualib/5.0/Match.ts new file mode 100644 index 000000000..3b1705939 --- /dev/null +++ b/src/lualib/5.0/Match.ts @@ -0,0 +1,12 @@ +/** @noSelfInFile */ + +export function __TS__Match(s: string, pattern: string, init?: number): LuaMultiReturn { + const [start, end, ...captures] = string.find(s, pattern, init); + if (start === undefined || end === undefined) { + return $multi(); + } else if (captures.length <= 0) { + return $multi(s.slice(start - 1, end)); + } else { + return $multi(...(captures as string[])); + } +} diff --git a/src/lualib/5.0/MathModf.ts b/src/lualib/5.0/MathModf.ts new file mode 100644 index 000000000..ec816ebe0 --- /dev/null +++ b/src/lualib/5.0/MathModf.ts @@ -0,0 +1,6 @@ +/** @noSelfInFile */ + +export function __TS__MathModf(x: number): LuaMultiReturn<[number, number]> { + const integral = x > 0 ? Math.floor(x) : Math.ceil(x); + return $multi(integral, x - integral); +} diff --git a/src/lualib/5.0/SparseArraySpread.ts b/src/lualib/5.0/SparseArraySpread.ts new file mode 100644 index 000000000..9a11f3cc9 --- /dev/null +++ b/src/lualib/5.0/SparseArraySpread.ts @@ -0,0 +1,6 @@ +import { __TS__SparseArray } from "./SparseArray"; +import { __TS__Unpack } from "./Unpack"; + +export function __TS__SparseArraySpread(this: void, sparseArray: __TS__SparseArray): LuaMultiReturn { + return __TS__Unpack(sparseArray, 1, sparseArray.sparseLength); +} diff --git a/src/lualib/5.0/Unpack.ts b/src/lualib/5.0/Unpack.ts new file mode 100644 index 000000000..a84416233 --- /dev/null +++ b/src/lualib/5.0/Unpack.ts @@ -0,0 +1,16 @@ +/** @noSelfInFile */ + +// We're not interested in emulating all of the behaviors of unpack() from Lua +// 5.1, just the ones needed by other parts of lualib. +export function __TS__Unpack(list: T[], i: number, j?: number): LuaMultiReturn { + if (i === 1 && j === undefined) { + return unpack(list); + } else { + j ??= list.length; + const slice: T[] = []; + for (let n = i; n <= j; n++) { + slice[n - i] = list[n - 1]; // We don't want to add 1 to the index into list. + } + return $multi(...slice); + } +} diff --git a/src/lualib/ArrayAt.ts b/src/lualib/ArrayAt.ts new file mode 100644 index 000000000..32e4ea2b4 --- /dev/null +++ b/src/lualib/ArrayAt.ts @@ -0,0 +1,13 @@ +// https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.at +// Technically the specs also allow non-numeric types to be passed as index. +// However, TypeScript types the `Array.at` index param as number so we also expect only numbers +// This behaviour also matches the implementation of other Array functions in lualib. +export function __TS__ArrayAt(this: T[], relativeIndex: number): T | undefined { + const absoluteIndex = relativeIndex < 0 ? this.length + relativeIndex : relativeIndex; + + if (absoluteIndex >= 0 && absoluteIndex < this.length) { + return this[absoluteIndex]; + } + + return undefined; +} diff --git a/src/lualib/ArrayConcat.ts b/src/lualib/ArrayConcat.ts index 06d5206b3..caaafa369 100644 --- a/src/lualib/ArrayConcat.ts +++ b/src/lualib/ArrayConcat.ts @@ -1,22 +1,22 @@ -declare function pcall(func: () => any): any; -declare function type(val: any): string; - -function __TS__ArrayConcat(arr1: any[], ...args: any[]): any[] { - const out: any[] = []; - for (const val of arr1) { - out[out.length] = val; - } - for (const arg of args) { - // Hack because we don't have an isArray function - if (pcall(() => (arg as any[]).length) && type(arg) !== "string") { - const argAsArray = (arg as any[]); - for (const val of argAsArray) { - out[out.length] = val; +export function __TS__ArrayConcat(this: T[], ...items: Array): T[] { + const result: T[] = []; + let len = 0; + for (const i of $range(1, this.length)) { + len++; + result[len - 1] = this[i - 1]; + } + for (const i of $range(1, items.length)) { + const item = items[i - 1]; + if (Array.isArray(item)) { + for (const j of $range(1, item.length)) { + len++; + result[len - 1] = item[j - 1]; + } + } else { + len++; + result[len - 1] = item; } - } else { - out[out.length] = arg; } - } - return out; + return result; } diff --git a/src/lualib/ArrayEntries.ts b/src/lualib/ArrayEntries.ts new file mode 100644 index 000000000..6cdef13a3 --- /dev/null +++ b/src/lualib/ArrayEntries.ts @@ -0,0 +1,14 @@ +// https://262.ecma-international.org/10.0/#sec-array.prototype.entries +export function __TS__ArrayEntries(this: void, array: T[]): IterableIterator<[number, T]> { + let key = 0; + return { + [Symbol.iterator](): IterableIterator<[number, T]> { + return this; + }, + next(): IteratorResult<[number, T]> { + const result = { done: array[key] === undefined, value: [key, array[key]] as [number, T] }; + key++; + return result; + }, + }; +} diff --git a/src/lualib/ArrayEvery.ts b/src/lualib/ArrayEvery.ts index e8434175a..d94efec54 100644 --- a/src/lualib/ArrayEvery.ts +++ b/src/lualib/ArrayEvery.ts @@ -1,6 +1,10 @@ -function __TS__ArrayEvery(arr: T[], callbackfn: (value: T, index?: number, array?: any[]) => boolean): boolean { - for (let i = 0; i < arr.length; i++) { - if (!callbackfn(arr[i], i, arr)) { +export function __TS__ArrayEvery( + this: T[], + callbackfn: (value: T, index?: number, array?: any[]) => boolean, + thisArg?: any +): boolean { + for (const i of $range(1, this.length)) { + if (!callbackfn.call(thisArg, this[i - 1], i - 1, this)) { return false; } } diff --git a/src/lualib/ArrayFill.ts b/src/lualib/ArrayFill.ts new file mode 100644 index 000000000..a1654114a --- /dev/null +++ b/src/lualib/ArrayFill.ts @@ -0,0 +1,19 @@ +// https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.fill +export function __TS__ArrayFill(this: T[], value: T, start?: number, end?: number): T[] { + let relativeStart = start ?? 0; + let relativeEnd = end ?? this.length; + + if (relativeStart < 0) { + relativeStart += this.length; + } + + if (relativeEnd < 0) { + relativeEnd += this.length; + } + + for (let i = relativeStart; i < relativeEnd; i++) { + this[i] = value; + } + + return this; +} diff --git a/src/lualib/ArrayFilter.ts b/src/lualib/ArrayFilter.ts index 8f0ac2fde..5d63a0494 100644 --- a/src/lualib/ArrayFilter.ts +++ b/src/lualib/ArrayFilter.ts @@ -1,8 +1,14 @@ -function __TS__ArrayFilter(arr: T[], callbackfn: (value: T, index?: number, array?: any[]) => boolean): T[] { +export function __TS__ArrayFilter( + this: T[], + callbackfn: (value: T, index?: number, array?: any[]) => boolean, + thisArg?: any +): T[] { const result: T[] = []; - for (let i = 0; i < arr.length; i++) { - if (callbackfn(arr[i], i, arr)) { - result[result.length] = arr[i]; + let len = 0; + for (const i of $range(1, this.length)) { + if (callbackfn.call(thisArg, this[i - 1], i - 1, this)) { + len++; + result[len - 1] = this[i - 1]; } } return result; diff --git a/src/lualib/ArrayFind.ts b/src/lualib/ArrayFind.ts new file mode 100644 index 000000000..3beb9c83b --- /dev/null +++ b/src/lualib/ArrayFind.ts @@ -0,0 +1,15 @@ +// https://www.ecma-international.org/ecma-262/10.0/index.html#sec-array.prototype.find +export function __TS__ArrayFind( + this: T[], + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any +): T | undefined { + for (const i of $range(1, this.length)) { + const elem = this[i - 1]; + if (predicate.call(thisArg, elem, i - 1, this)) { + return elem; + } + } + + return undefined; +} diff --git a/src/lualib/ArrayFindIndex.ts b/src/lualib/ArrayFindIndex.ts new file mode 100644 index 000000000..3741f3a0f --- /dev/null +++ b/src/lualib/ArrayFindIndex.ts @@ -0,0 +1,12 @@ +export function __TS__ArrayFindIndex( + this: T[], + callbackFn: (element: T, index?: number, array?: T[]) => boolean, + thisArg?: any +): number { + for (const i of $range(1, this.length)) { + if (callbackFn.call(thisArg, this[i - 1], i - 1, this)) { + return i - 1; + } + } + return -1; +} diff --git a/src/lualib/ArrayFlat.ts b/src/lualib/ArrayFlat.ts new file mode 100644 index 000000000..3e15b455b --- /dev/null +++ b/src/lualib/ArrayFlat.ts @@ -0,0 +1,25 @@ +export function __TS__ArrayFlat(this: any[], depth = 1): any[] { + const result: any[] = []; + let len = 0; + for (const i of $range(1, this.length)) { + const value = this[i - 1]; + if (depth > 0 && Array.isArray(value)) { + let toAdd: any[]; + if (depth === 1) { + toAdd = value; + } else { + toAdd = value.flat(depth - 1); + } + for (const j of $range(1, toAdd.length)) { + const val = toAdd[j - 1]; + len++; + result[len - 1] = val; + } + } else { + len++; + result[len - 1] = value; + } + } + + return result; +} diff --git a/src/lualib/ArrayFlatMap.ts b/src/lualib/ArrayFlatMap.ts new file mode 100644 index 000000000..fa68fce8f --- /dev/null +++ b/src/lualib/ArrayFlatMap.ts @@ -0,0 +1,22 @@ +export function __TS__ArrayFlatMap( + this: T[], + callback: (value: T, index: number, array: T[]) => U | readonly U[], + thisArg?: any +): U[] { + const result: U[] = []; + let len = 0; + for (const i of $range(1, this.length)) { + const value = callback.call(thisArg, this[i - 1], i - 1, this); + if (Array.isArray(value)) { + for (const j of $range(1, value.length)) { + len++; + result[len - 1] = value[j - 1]; + } + } else { + len++; + result[len - 1] = value as U; + } + } + + return result; +} diff --git a/src/lualib/ArrayForEach.ts b/src/lualib/ArrayForEach.ts index 3001915f0..caf4ec53e 100644 --- a/src/lualib/ArrayForEach.ts +++ b/src/lualib/ArrayForEach.ts @@ -1,5 +1,9 @@ -function __TS__ArrayForEach(arr: T[], callbackFn: (value: T, index?: number, array?: any[]) => any): void { - for (let i = 0; i < arr.length; i++) { - callbackFn(arr[i], i, arr); +export function __TS__ArrayForEach( + this: T[], + callbackFn: (value: T, index?: number, array?: any[]) => any, + thisArg?: any +): void { + for (const i of $range(1, this.length)) { + callbackFn.call(thisArg, this[i - 1], i - 1, this); } } diff --git a/src/lualib/ArrayFrom.ts b/src/lualib/ArrayFrom.ts new file mode 100644 index 000000000..3742d7949 --- /dev/null +++ b/src/lualib/ArrayFrom.ts @@ -0,0 +1,37 @@ +/** @noSelfInFile */ + +import { __TS__Iterator } from "./Iterator"; + +function arrayLikeStep(this: ArrayLike, index: number): LuaMultiReturn<[number, unknown] | []> { + index += 1; + if (index > this.length) return $multi(); + return $multi(index, this[index]); +} + +const arrayLikeIterator: ( + this: void, + arr: ArrayLike | Iterable +) => LuaIterable> = ((arr: any) => { + if (typeof arr.length === "number") return $multi(arrayLikeStep, arr, 0); + return __TS__Iterator(arr); +}) as any; + +export function __TS__ArrayFrom( + this: void, + arrayLike: ArrayLike | Iterable, + mapFn?: (this: unknown, element: unknown, index: number) => unknown, + thisArg?: unknown +): unknown[] { + const result = []; + if (mapFn === undefined) { + for (const [, v] of arrayLikeIterator(arrayLike)) { + result.push(v); + } + } else { + let i = 0; + for (const [, v] of arrayLikeIterator(arrayLike)) { + result.push(mapFn.call(thisArg, v, i++)); + } + } + return result; +} diff --git a/src/lualib/ArrayIncludes.ts b/src/lualib/ArrayIncludes.ts new file mode 100644 index 000000000..b3fd871f6 --- /dev/null +++ b/src/lualib/ArrayIncludes.ts @@ -0,0 +1,21 @@ +// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.includes +export function __TS__ArrayIncludes(this: T[], searchElement: T, fromIndex = 0): boolean { + const len = this.length; + let k = fromIndex; + + if (fromIndex < 0) { + k = len + fromIndex; + } + + if (k < 0) { + k = 0; + } + + for (const i of $range(k + 1, len)) { + if (this[i - 1] === searchElement) { + return true; + } + } + + return false; +} diff --git a/src/lualib/ArrayIndexOf.ts b/src/lualib/ArrayIndexOf.ts index c50ce5ded..0a3ebc1f9 100644 --- a/src/lualib/ArrayIndexOf.ts +++ b/src/lualib/ArrayIndexOf.ts @@ -1,31 +1,23 @@ -function __TS__ArrayIndexOf(arr: T[], searchElement: T, fromIndex?: number): number { - const len = arr.length; +export function __TS__ArrayIndexOf(this: T[], searchElement: T, fromIndex = 0): number { + const len = this.length; if (len === 0) { return -1; } - let n = 0; - if (fromIndex) { - n = fromIndex; - } - - if (n >= len) { + if (fromIndex >= len) { return -1; } - let k; - if (n >= 0) { - k = n; - } else { - k = len + n; - if (k < 0) { - k = 0; + if (fromIndex < 0) { + fromIndex = len + fromIndex; + if (fromIndex < 0) { + fromIndex = 0; } } - for (let i = k; i < len; i++) { - if (arr[i] === searchElement) { - return i; + for (const i of $range(fromIndex + 1, len)) { + if (this[i - 1] === searchElement) { + return i - 1; } } diff --git a/src/lualib/ArrayIsArray.ts b/src/lualib/ArrayIsArray.ts new file mode 100644 index 000000000..61fca760b --- /dev/null +++ b/src/lualib/ArrayIsArray.ts @@ -0,0 +1,7 @@ +declare type NextEmptyCheck = (this: void, table: any, index?: undefined) => unknown | undefined; + +export function __TS__ArrayIsArray(this: void, value: any): value is any[] { + // Workaround to determine if value is an array or not (fails in case of objects without keys) + // See discussion in: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/737 + return type(value) === "table" && (1 in value || (next as NextEmptyCheck)(value) === undefined); +} diff --git a/src/lualib/ArrayJoin.ts b/src/lualib/ArrayJoin.ts new file mode 100644 index 000000000..0f0b11c9f --- /dev/null +++ b/src/lualib/ArrayJoin.ts @@ -0,0 +1,7 @@ +export function __TS__ArrayJoin(this: any[], separator = ",") { + const parts: string[] = []; + for (const i of $range(1, this.length)) { + parts[i - 1] = this[i - 1].toString(); + } + return table.concat(parts, separator); +} diff --git a/src/lualib/ArrayMap.ts b/src/lualib/ArrayMap.ts index f7ea7aa50..2caf84028 100644 --- a/src/lualib/ArrayMap.ts +++ b/src/lualib/ArrayMap.ts @@ -1,7 +1,11 @@ -function __TS__ArrayMap(arr: T[], callbackfn: (value: T, index?: number, array?: T[]) => U): U[] { - const newArray: U[] = []; - for (let i = 0; i < arr.length; i++) { - newArray[i] = callbackfn(arr[i], i, arr); +export function __TS__ArrayMap( + this: T[], + callbackfn: (value: T, index?: number, array?: T[]) => U, + thisArg?: any +): U[] { + const result: U[] = []; + for (const i of $range(1, this.length)) { + result[i - 1] = callbackfn.call(thisArg, this[i - 1], i - 1, this); } - return newArray; + return result; } diff --git a/src/lualib/ArrayPush.ts b/src/lualib/ArrayPush.ts index 8e1d8e324..9f181d1f2 100644 --- a/src/lualib/ArrayPush.ts +++ b/src/lualib/ArrayPush.ts @@ -1,6 +1,8 @@ -function __TS__ArrayPush(arr: T[], ...items: T[]): number { - for (const item of items) { - arr[arr.length] = item; +export function __TS__ArrayPush(this: T[], ...items: T[]): number { + let len = this.length; + for (const i of $range(1, items.length)) { + len++; + this[len - 1] = items[i - 1]; } - return arr.length; + return len; } diff --git a/src/lualib/ArrayPushArray.ts b/src/lualib/ArrayPushArray.ts new file mode 100644 index 000000000..8ea9b6916 --- /dev/null +++ b/src/lualib/ArrayPushArray.ts @@ -0,0 +1,8 @@ +export function __TS__ArrayPushArray(this: T[], items: T[]): number { + let len = this.length; + for (const i of $range(1, items.length)) { + len++; + this[len - 1] = items[i - 1]; + } + return len; +} diff --git a/src/lualib/ArrayReduce.ts b/src/lualib/ArrayReduce.ts new file mode 100644 index 000000000..cc569409a --- /dev/null +++ b/src/lualib/ArrayReduce.ts @@ -0,0 +1,29 @@ +import { __TS__CountVarargs } from "./CountVarargs"; + +// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.reduce +export function __TS__ArrayReduce( + this: TElement[], + callbackFn: (accumulator: TAccumulator, currentValue: TElement, index: number, array: TElement[]) => TAccumulator, + ...initial: TAccumulator[] +): TAccumulator { + const len = this.length; + + let k = 0; + let accumulator: TAccumulator = undefined!; + + // Check if initial value is present in function call + if (__TS__CountVarargs(...initial) !== 0) { + [accumulator] = [...initial]; + } else if (len > 0) { + accumulator = this[0] as unknown as TAccumulator; + k = 1; + } else { + throw "Reduce of empty array with no initial value"; + } + + for (const i of $range(k + 1, len)) { + accumulator = callbackFn(accumulator, this[i - 1], i - 1, this); + } + + return accumulator; +} diff --git a/src/lualib/ArrayReduceRight.ts b/src/lualib/ArrayReduceRight.ts new file mode 100644 index 000000000..2316fa048 --- /dev/null +++ b/src/lualib/ArrayReduceRight.ts @@ -0,0 +1,29 @@ +import { __TS__CountVarargs } from "./CountVarargs"; + +// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.reduce +export function __TS__ArrayReduceRight( + this: TElement[], + callbackFn: (accumulator: TAccumulator, currentValue: TElement, index: number, array: TElement[]) => TAccumulator, + ...initial: TAccumulator[] +): TAccumulator { + const len = this.length; + + let k = len - 1; + let accumulator: TAccumulator = undefined!; + + // Check if initial value is present in function call + if (__TS__CountVarargs(...initial) !== 0) { + [accumulator] = [...initial]; + } else if (len > 0) { + accumulator = this[k] as unknown as TAccumulator; + k -= 1; + } else { + throw "Reduce of empty array with no initial value"; + } + + for (const i of $range(k + 1, 1, -1)) { + accumulator = callbackFn(accumulator, this[i - 1], i - 1, this); + } + + return accumulator; +} diff --git a/src/lualib/ArrayReverse.ts b/src/lualib/ArrayReverse.ts index 3c4839417..96403753b 100644 --- a/src/lualib/ArrayReverse.ts +++ b/src/lualib/ArrayReverse.ts @@ -1,12 +1,12 @@ -function __TS__ArrayReverse(arr: any[]): any[] { - let i = 0; - let j = arr.length - 1; +export function __TS__ArrayReverse(this: any[]): any[] { + let i = 1; + let j = this.length; while (i < j) { - const temp = arr[j]; - arr[j] = arr[i]; - arr[i] = temp; - i = i + 1; - j = j - 1; + const temp = this[j - 1]; + this[j - 1] = this[i - 1]; + this[i - 1] = temp; + i++; + j--; } - return arr; + return this; } diff --git a/src/lualib/ArraySetLength.ts b/src/lualib/ArraySetLength.ts new file mode 100644 index 000000000..6f4de4b18 --- /dev/null +++ b/src/lualib/ArraySetLength.ts @@ -0,0 +1,15 @@ +export function __TS__ArraySetLength(this: T[], length: number): number { + if ( + length < 0 || + length !== length || // NaN + length === Infinity || // Infinity + Math.floor(length) !== length + ) { + // non-integer + throw `invalid array length: ${length}`; + } + for (const i of $range(length + 1, this.length)) { + this[i - 1] = undefined!; + } + return length; +} diff --git a/src/lualib/ArrayShift.ts b/src/lualib/ArrayShift.ts deleted file mode 100644 index a95df1a49..000000000 --- a/src/lualib/ArrayShift.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace table { - function remove(arr: T[], idx: number): T; -} -function __TS__ArrayShift(arr: T[]): T { - return table.remove(arr, 1); -} diff --git a/src/lualib/ArraySlice.ts b/src/lualib/ArraySlice.ts index f3eb5b3d6..e8c357e7f 100644 --- a/src/lualib/ArraySlice.ts +++ b/src/lualib/ArraySlice.ts @@ -1,32 +1,39 @@ -// https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf 22.1.3.23 -function __TS__ArraySlice(list: T[], first: number, last: number): T[] { - const len = list.length; +// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.slice +export function __TS__ArraySlice(this: T[], first?: number, last?: number): T[] { + const len = this.length; - let k: number; + first = first ?? 0; if (first < 0) { - k = Math.max(len + first, 0); + first = len + first; + if (first < 0) { + first = 0; + } } else { - k = Math.min(first, len); + if (first > len) { + first = len; + } } - let relativeEnd = last; - if (last === undefined) { - relativeEnd = len; - } - - let final: number; - if (relativeEnd < 0) { - final = Math.max(len + relativeEnd, 0); + last = last ?? len; + if (last < 0) { + last = len + last; + if (last < 0) { + last = 0; + } } else { - final = Math.min(relativeEnd, len); + if (last > len) { + last = len; + } } const out = []; - let n = 0; - while (k < final) { - out[n] = list[k]; - k++; + first++; + last++; + let n = 1; + while (first < last) { + out[n - 1] = this[first - 1]; + first++; n++; } return out; diff --git a/src/lualib/ArraySome.ts b/src/lualib/ArraySome.ts index d03e7a9fe..33ffe69ab 100644 --- a/src/lualib/ArraySome.ts +++ b/src/lualib/ArraySome.ts @@ -1,6 +1,10 @@ -function __TS__ArraySome(arr: T[], callbackfn: (value: T, index?: number, array?: any[]) => boolean): boolean { - for (let i = 0; i < arr.length; i++) { - if (callbackfn(arr[i], i, arr)) { +export function __TS__ArraySome( + this: T[], + callbackfn: (value: T, index?: number, array?: any[]) => boolean, + thisArg?: any +): boolean { + for (const i of $range(1, this.length)) { + if (callbackfn.call(thisArg, this[i - 1], i - 1, this)) { return true; } } diff --git a/src/lualib/ArraySort.ts b/src/lualib/ArraySort.ts index 18745695c..2a6495511 100644 --- a/src/lualib/ArraySort.ts +++ b/src/lualib/ArraySort.ts @@ -1,7 +1,8 @@ -declare namespace table { - function sort(arr: T[], compareFn?: (a: T, b: T) => number): void; -} -function __TS__ArraySort(arr: T[], compareFn?: (a: T, b: T) => number): T[] { - table.sort(arr, compareFn); - return arr; +export function __TS__ArraySort(this: T[], compareFn?: (a: T, b: T) => number): T[] { + if (compareFn !== undefined) { + table.sort(this, (a, b) => compareFn(a, b) < 0); + } else { + table.sort(this); + } + return this; } diff --git a/src/lualib/ArraySplice.ts b/src/lualib/ArraySplice.ts index b875d706c..24b5264b2 100644 --- a/src/lualib/ArraySplice.ts +++ b/src/lualib/ArraySplice.ts @@ -1,73 +1,90 @@ -function __TS__ArraySplice(list: T[], start: number, deleteCount: number, ...items: T[]): T[] { +import { __TS__CountVarargs } from "./CountVarargs"; - const len = list.length; +// https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.splice +export function __TS__ArraySplice(this: T[], ...args: any[]): T[] { + const len = this.length; - let actualStart; + const actualArgumentCount = __TS__CountVarargs(...args); + let start = args[0] as number; + const deleteCount = args[1] as number; - if (start < 0) { - actualStart = Math.max(len + start, 0); - } else { - actualStart = Math.min(start, len); + if (start < 0) { + start = len + start; + if (start < 0) { + start = 0; + } + } else if (start > len) { + start = len; } - const itemCount = items.length; + let itemCount = actualArgumentCount - 2; + if (itemCount < 0) { + itemCount = 0; + } let actualDeleteCount: number; - if (!start) { + if (actualArgumentCount === 0) { + // ECMA-spec line 5: if number of actual arguments is 0 actualDeleteCount = 0; - } else if (!deleteCount) { - actualDeleteCount = len - actualStart; - } else { - actualDeleteCount = Math.min(Math.max(deleteCount, 0), len - actualStart); + } else if (actualArgumentCount === 1) { + // ECMA-spec line 6: if number of actual arguments is 1 + actualDeleteCount = len - start; + } else { + actualDeleteCount = deleteCount ?? 0; + if (actualDeleteCount < 0) { + actualDeleteCount = 0; + } + if (actualDeleteCount > len - start) { + actualDeleteCount = len - start; + } } const out: T[] = []; - for (let k = 0; k < actualDeleteCount; k++) { - const from = actualStart + k; + for (const k of $range(1, actualDeleteCount)) { + const from = start + k; - if (list[from]) { - out[k] = list[from]; + if (this[from - 1] !== undefined) { + out[k - 1] = this[from - 1]; } } if (itemCount < actualDeleteCount) { - for (let k = actualStart; k < len - actualDeleteCount; k++) { + for (const k of $range(start + 1, len - actualDeleteCount)) { const from = k + actualDeleteCount; const to = k + itemCount; - if (list[from]) { - list[to] = list[from]; + if (this[from - 1]) { + this[to - 1] = this[from - 1]; } else { - list[to] = undefined; + this[to - 1] = undefined!; } } - for (let k = len; k > len - actualDeleteCount + itemCount; k--) { - list[k - 1] = undefined; + for (const k of $range(len - actualDeleteCount + itemCount + 1, len)) { + this[k - 1] = undefined!; } } else if (itemCount > actualDeleteCount) { + for (const k of $range(len - actualDeleteCount, start + 1, -1)) { + const from = k + actualDeleteCount; + const to = k + itemCount; - for (let k = len - actualDeleteCount; k > actualStart; k--) { - const from = k + actualDeleteCount - 1; - const to = k + itemCount - 1; - - if (list[from]) { - list[to] = list[from]; + if (this[from - 1]) { + this[to - 1] = this[from - 1]; } else { - list[to] = undefined; + this[to - 1] = undefined!; } } } - let j = actualStart; - for (const e of items) { - list[j] = e; + let j = start + 1; + for (const i of $range(3, actualArgumentCount)) { + this[j - 1] = args[i - 1]; j++; } - for (let k = list.length - 1; k >= len - actualDeleteCount + itemCount; k--) { - list[k] = undefined; + for (const k of $range(this.length, len - actualDeleteCount + itemCount + 1, -1)) { + this[k - 1] = undefined!; } return out; diff --git a/src/lualib/ArrayToObject.ts b/src/lualib/ArrayToObject.ts new file mode 100644 index 000000000..1f9e9778a --- /dev/null +++ b/src/lualib/ArrayToObject.ts @@ -0,0 +1,7 @@ +export function __TS__ArrayToObject(this: T[]): Record { + const object: Record = {}; + for (const i of $range(1, this.length)) { + object[i - 1] = this[i - 1]; + } + return object; +} diff --git a/src/lualib/ArrayToReversed.ts b/src/lualib/ArrayToReversed.ts new file mode 100644 index 000000000..50b58855f --- /dev/null +++ b/src/lualib/ArrayToReversed.ts @@ -0,0 +1,5 @@ +export function __TS__ArrayToReversed(this: T[]): T[] { + const copy = [...this]; + copy.reverse(); + return copy; +} diff --git a/src/lualib/ArrayToSorted.ts b/src/lualib/ArrayToSorted.ts new file mode 100644 index 000000000..47c033d2d --- /dev/null +++ b/src/lualib/ArrayToSorted.ts @@ -0,0 +1,5 @@ +export function __TS__ArrayToSorted(this: T[], compareFn?: (a: T, b: T) => number): T[] { + const copy = [...this]; + copy.sort(compareFn); + return copy; +} diff --git a/src/lualib/ArrayToSpliced.ts b/src/lualib/ArrayToSpliced.ts new file mode 100644 index 000000000..298ed8035 --- /dev/null +++ b/src/lualib/ArrayToSpliced.ts @@ -0,0 +1,5 @@ +export function __TS__ArrayToSpliced(this: T[], start: number, deleteCount: number, ...items: T[]): T[] { + const copy = [...this]; + copy.splice(start, deleteCount, ...items); + return copy; +} diff --git a/src/lualib/ArrayUnshift.ts b/src/lualib/ArrayUnshift.ts index cb0031100..6c628c625 100644 --- a/src/lualib/ArrayUnshift.ts +++ b/src/lualib/ArrayUnshift.ts @@ -1,9 +1,12 @@ -declare namespace table { - function insert(arr: T[], idx: number, val: T): void; -} -function __TS__ArrayUnshift(arr: T[], ...items: T[]): number { - for (let i = items.length - 1; i >= 0; --i) { - table.insert(arr, 1, items[i]); +export function __TS__ArrayUnshift(this: T[], ...items: T[]): number { + const numItemsToInsert = items.length; + if (numItemsToInsert === 0) return this.length; + + for (const i of $range(this.length, 1, -1)) { + this[i + numItemsToInsert - 1] = this[i - 1]; + } + for (const i of $range(1, numItemsToInsert)) { + this[i - 1] = items[i - 1]; } - return arr.length; + return this.length; } diff --git a/src/lualib/ArrayWith.ts b/src/lualib/ArrayWith.ts new file mode 100644 index 000000000..90d818a5a --- /dev/null +++ b/src/lualib/ArrayWith.ts @@ -0,0 +1,5 @@ +export function __TS__ArrayWith(this: T[], index: number, value: T): T[] { + const copy = [...this]; + copy[index] = value; + return copy; +} diff --git a/src/lualib/Await.ts b/src/lualib/Await.ts new file mode 100644 index 000000000..7f6a345ee --- /dev/null +++ b/src/lualib/Await.ts @@ -0,0 +1,69 @@ +// The following is a translation of the TypeScript async awaiter which uses generators and yields. +// For Lua we use coroutines instead. +// +// Source: +// +// var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { +// function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } +// return new (P || (P = Promise))(function (resolve, reject) { +// function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } +// function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } +// function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } +// step((generator = generator.apply(thisArg, _arguments || [])).next()); +// }); +// }; +// + +import { __TS__Promise } from "./Promise"; + +const coroutine = _G.coroutine ?? {}; +const cocreate = coroutine.create; +const coresume = coroutine.resume; +const costatus = coroutine.status; +const coyield = coroutine.yield; + +// Be extremely careful editing this function. A single non-tail function call may ruin chained awaits performance +// eslint-disable-next-line @typescript-eslint/promise-function-async +export function __TS__AsyncAwaiter(this: void, generator: (this: void) => void) { + return new Promise((resolve, reject) => { + let resolved = false; + const asyncCoroutine = cocreate(generator); + + function fulfilled(value: unknown): void { + const [success, resultOrError] = coresume(asyncCoroutine, value); + if (success) { + // `step` never throws. Tail call return is important! + return step(resultOrError); + } + // `reject` should never throw. Tail call return is important! + return reject(resultOrError); + } + + function step(this: void, result: unknown): void { + if (resolved) { + return; + } + if (costatus(asyncCoroutine) === "dead") { + // `resolve` never throws. Tail call return is important! + return resolve(result); + } + // We cannot use `then` because we need to avoid calling `coroutine.resume` from inside `pcall` + // `fulfilled` and `reject` should never throw. Tail call return is important! + return __TS__Promise.resolve(result).addCallbacks(fulfilled, reject); + } + + const [success, resultOrError] = coresume(asyncCoroutine, (v: unknown) => { + resolved = true; + return __TS__Promise.resolve(v).addCallbacks(resolve, reject); + }); + if (success) { + return step(resultOrError); + } else { + return reject(resultOrError); + } + }); +} + +export function __TS__Await(this: void, thing: unknown) { + return coyield(thing); +} diff --git a/src/lualib/Class.ts b/src/lualib/Class.ts new file mode 100644 index 000000000..bdcd7ce4f --- /dev/null +++ b/src/lualib/Class.ts @@ -0,0 +1,6 @@ +export function __TS__Class(): LuaClass { + const c: LuaClass = { prototype: {} }; + c.prototype.__index = c.prototype; + c.prototype.constructor = c; + return c; +} diff --git a/src/lualib/ClassExtends.ts b/src/lualib/ClassExtends.ts new file mode 100644 index 000000000..6075868d7 --- /dev/null +++ b/src/lualib/ClassExtends.ts @@ -0,0 +1,20 @@ +export function __TS__ClassExtends(this: void, target: LuaClass, base: LuaClass): void { + target.____super = base; + + // Set base class as a metatable, because descriptors use `getmetatable` to get extended prototype + const staticMetatable: any = setmetatable({ __index: base }, base); + setmetatable(target, staticMetatable); + + const baseMetatable = getmetatable(base); + if (baseMetatable) { + // Re-add metatable events defined by descriptors + if (typeof baseMetatable.__index === "function") staticMetatable.__index = baseMetatable.__index; + if (typeof baseMetatable.__newindex === "function") staticMetatable.__newindex = baseMetatable.__newindex; + } + + setmetatable(target.prototype, base.prototype); + // Re-add metatable events defined by accessors with `__TS__SetDescriptor` + if (typeof base.prototype.__index === "function") target.prototype.__index = base.prototype.__index; + if (typeof base.prototype.__newindex === "function") target.prototype.__newindex = base.prototype.__newindex; + if (typeof base.prototype.__tostring === "function") target.prototype.__tostring = base.prototype.__tostring; +} diff --git a/src/lualib/ClassIndex.ts b/src/lualib/ClassIndex.ts deleted file mode 100644 index f879fd9aa..000000000 --- a/src/lualib/ClassIndex.ts +++ /dev/null @@ -1,28 +0,0 @@ -interface LuaClass { - ____super?: LuaClass; - ____getters?: { [key: string]: (self: LuaClass) => any }; -} - -declare function rawget(obj: T, key: K): T[K]; - -function __TS__ClassIndex(classTable: LuaClass, key: keyof LuaClass): any { - while (true) { - const getters = rawget(classTable, "____getters"); - if (getters) { - const getter = getters[key]; - if (getter) { - return getter(classTable); - } - } - - classTable = rawget(classTable, "____super"); - if (!classTable) { - break; - } - - const val = rawget(classTable, key); - if (val !== null) { - return val; - } - } -} diff --git a/src/lualib/ClassNewIndex.ts b/src/lualib/ClassNewIndex.ts deleted file mode 100644 index 65108c6a2..000000000 --- a/src/lualib/ClassNewIndex.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface LuaClass { - ____super?: LuaClass; - ____setters?: { [key: string]: (self: LuaClass, val: any) => void }; -} - -declare function rawget(obj: T, key: K): T[K]; -declare function rawset(obj: T, key: K, val: T[K]): void; - -function __TS__ClassNewIndex(classTable: LuaClass, key: keyof LuaClass, val: any): void { - let tbl = classTable; - do { - const setters = rawget(tbl, "____setters"); - if (setters) { - const setter = setters[key]; - if (setter) { - setter(tbl, val); - return; - } - } - - tbl = rawget(tbl, "____super"); - } - while (tbl); - - rawset(classTable, key, val); -} diff --git a/src/lualib/CloneDescriptor.ts b/src/lualib/CloneDescriptor.ts new file mode 100644 index 000000000..2afe76914 --- /dev/null +++ b/src/lualib/CloneDescriptor.ts @@ -0,0 +1,26 @@ +export function __TS__CloneDescriptor( + this: void, + { enumerable, configurable, get, set, writable, value }: PropertyDescriptor +): PropertyDescriptor { + const descriptor: PropertyDescriptor = { + enumerable: enumerable === true, + configurable: configurable === true, + }; + + const hasGetterOrSetter = get !== undefined || set !== undefined; + const hasValueOrWritableAttribute = writable !== undefined || value !== undefined; + + if (hasGetterOrSetter && hasValueOrWritableAttribute) { + throw "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute."; + } + + if (get || set) { + descriptor.get = get; + descriptor.set = set; + } else { + descriptor.value = value; + descriptor.writable = writable === true; + } + + return descriptor; +} diff --git a/src/lualib/Decorate.ts b/src/lualib/Decorate.ts new file mode 100644 index 000000000..fd65f44e8 --- /dev/null +++ b/src/lualib/Decorate.ts @@ -0,0 +1,22 @@ +/** + * TypeScript 5.0 decorators + */ +import { Decorator } from "./Decorator"; + +export function __TS__Decorate( + this: TClass, + originalValue: TTarget, + decorators: Array>, + context: DecoratorContext +): TTarget { + let result = originalValue; + + for (let i = decorators.length; i >= 0; i--) { + const decorator = decorators[i]; + if (decorator !== undefined) { + result = decorator.call(this, result, context) ?? result; + } + } + + return result; +} diff --git a/src/lualib/DecorateLegacy.ts b/src/lualib/DecorateLegacy.ts new file mode 100644 index 000000000..0369623d3 --- /dev/null +++ b/src/lualib/DecorateLegacy.ts @@ -0,0 +1,54 @@ +/** + * Old-style decorators, activated by enabling the experimentalDecorators flag + */ +import { __TS__ObjectGetOwnPropertyDescriptor } from "./ObjectGetOwnPropertyDescriptor"; +import { __TS__SetDescriptor } from "./SetDescriptor"; + +export type LegacyDecorator = ( + target: TTarget, + key?: TKey, + descriptor?: PropertyDescriptor +) => TTarget; + +export function __TS__DecorateLegacy( + this: void, + decorators: Array>, + target: TTarget, + key?: TKey, + desc?: any +): TTarget { + let result = target; + + for (let i = decorators.length; i >= 0; i--) { + const decorator = decorators[i]; + if (decorator !== undefined) { + const oldResult = result; + + if (key === undefined) { + result = decorator(result); + } else if (desc === true) { + const value = rawget(target, key); + const descriptor = __TS__ObjectGetOwnPropertyDescriptor(target, key) ?? { + configurable: true, + writable: true, + value, + }; + const desc = decorator(target, key, descriptor) || descriptor; + const isSimpleValue = desc.configurable === true && desc.writable === true && !desc.get && !desc.set; + if (isSimpleValue) { + rawset(target, key, desc.value); + } else { + __TS__SetDescriptor(target, key, { ...descriptor, ...desc }); + } + } else if (desc === false) { + result = decorator(target, key, desc); + } else { + result = decorator(target, key); + } + + result = result || oldResult; + } + } + + return result; +} diff --git a/src/lualib/DecorateParam.ts b/src/lualib/DecorateParam.ts new file mode 100644 index 000000000..0165294b2 --- /dev/null +++ b/src/lualib/DecorateParam.ts @@ -0,0 +1,14 @@ +import type { LegacyDecorator } from "./DecorateLegacy"; + +type ParamDecorator = ( + target: TTarget, + key: TKey | undefined, + index: number +) => TTarget; +export function __TS__DecorateParam( + this: void, + paramIndex: number, + decorator: ParamDecorator +): LegacyDecorator { + return (target: TTarget, key?: TKey) => decorator(target, key, paramIndex); +} diff --git a/src/lualib/Decorator.d.ts b/src/lualib/Decorator.d.ts new file mode 100644 index 000000000..d8f84febf --- /dev/null +++ b/src/lualib/Decorator.d.ts @@ -0,0 +1 @@ +export type Decorator = (target: TTarget, context: DecoratorContext) => TTarget; diff --git a/src/lualib/DelegatedYield.ts b/src/lualib/DelegatedYield.ts new file mode 100644 index 000000000..827dd6326 --- /dev/null +++ b/src/lualib/DelegatedYield.ts @@ -0,0 +1,34 @@ +import { GeneratorIterator } from "./GeneratorIterator"; + +export function __TS__DelegatedYield(this: void, iterable: string | GeneratorIterator | Iterable | readonly T[]) { + if (typeof iterable === "string") { + for (const index of $range(0, iterable.length - 1)) { + coroutine.yield(iterable[index]); + } + } else if ("____coroutine" in iterable) { + const co = iterable.____coroutine; + while (true) { + const [status, value] = coroutine.resume(co); + if (!status) throw value; + if (coroutine.status(co) === "dead") { + return value; + } else { + coroutine.yield(value); + } + } + } else if (iterable[Symbol.iterator]) { + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + return result.value; + } else { + coroutine.yield(result.value); + } + } + } else { + for (const value of iterable as readonly T[]) { + coroutine.yield(value); + } + } +} diff --git a/src/lualib/Delete.ts b/src/lualib/Delete.ts new file mode 100644 index 000000000..07499f44f --- /dev/null +++ b/src/lualib/Delete.ts @@ -0,0 +1,17 @@ +import { __TS__ObjectGetOwnPropertyDescriptors } from "./ObjectGetOwnPropertyDescriptors"; + +export function __TS__Delete(this: void, target: any, key: any): boolean { + const descriptors = __TS__ObjectGetOwnPropertyDescriptors(target); + const descriptor = descriptors[key]; + if (descriptor) { + if (!descriptor.configurable) { + throw new TypeError(`Cannot delete property ${key} of ${target}.`); + } + + descriptors[key] = undefined; + return true; + } + + target[key] = undefined; + return true; +} diff --git a/src/lualib/DescriptorGet.ts b/src/lualib/DescriptorGet.ts new file mode 100644 index 000000000..034a5825a --- /dev/null +++ b/src/lualib/DescriptorGet.ts @@ -0,0 +1,25 @@ +const getmetatable = _G.getmetatable; +const rawget = _G.rawget; + +export function __TS__DescriptorGet(this: any, metatable: any, key: string): void { + while (metatable) { + const rawResult = rawget(metatable, key as any); + if (rawResult !== undefined) { + return rawResult; + } + + const descriptors = rawget(metatable, "_descriptors"); + if (descriptors) { + const descriptor: PropertyDescriptor = descriptors[key]; + if (descriptor !== undefined) { + if (descriptor.get) { + return descriptor.get.call(this); + } + + return descriptor.value; + } + } + + metatable = getmetatable(metatable); + } +} diff --git a/src/lualib/DescriptorSet.ts b/src/lualib/DescriptorSet.ts new file mode 100644 index 000000000..bb50ca4d3 --- /dev/null +++ b/src/lualib/DescriptorSet.ts @@ -0,0 +1,28 @@ +const getmetatable = _G.getmetatable; +const rawget = _G.rawget; +const rawset = _G.rawset; + +export function __TS__DescriptorSet(this: any, metatable: any, key: string, value: any): void { + while (metatable) { + const descriptors = rawget(metatable, "_descriptors"); + if (descriptors) { + const descriptor: PropertyDescriptor = descriptors[key]; + if (descriptor !== undefined) { + if (descriptor.set) { + descriptor.set.call(this, value); + } else { + if (descriptor.writable === false) { + throw `Cannot assign to read only property '${key}' of object '${this}'`; + } + + descriptor.value = value; + } + return; + } + } + + metatable = getmetatable(metatable); + } + + rawset(this, key, value); +} diff --git a/src/lualib/Error.ts b/src/lualib/Error.ts new file mode 100644 index 000000000..ae5deb05b --- /dev/null +++ b/src/lualib/Error.ts @@ -0,0 +1,93 @@ +import { __TS__New } from "./New"; + +interface ErrorType { + name: string; + new (...args: any[]): Error; +} + +function getErrorStack(constructor: () => any): string | undefined { + // If debug module is not available in this environment, don't bother trying to get stack trace + if (debug === undefined) return undefined; + + let level = 1; + while (true) { + const info = debug.getinfo(level, "f"); + level += 1; + if (!info) { + // constructor is not in call stack + level = 1; + break; + } else if (info.func === constructor) { + break; + } + } + + 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); + } +} + +function wrapErrorToString(getDescription: (this: T) => string): (this: T) => string { + return function (this: Error): 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"); + if (isClassicLua || (caller && caller.func !== error)) { + return description; + } else { + return `${description}\n${this.stack}`; + } + }; +} + +function initErrorClass(Type: ErrorType, name: string): any { + Type.name = name; + return setmetatable(Type, { + __call: (_self: any, message: string) => new Type(message), + }); +} + +export const Error: ErrorConstructor = initErrorClass( + class implements Error { + public name = "Error"; + public stack?: string; + + constructor(public message = "") { + this.stack = getErrorStack(__TS__New as any); + const metatable = getmetatable(this); + if (metatable && !metatable.__errorToStringPatched) { + metatable.__errorToStringPatched = true; + metatable.__tostring = wrapErrorToString(metatable.__tostring); + } + } + + public toString(): string { + return this.message !== "" ? `${this.name}: ${this.message}` : this.name; + } + }, + "Error" +); + +function createErrorClass(name: string) { + return initErrorClass( + class extends Error { + public name = name; + }, + name + ); +} + +export const RangeError = createErrorClass("RangeError"); +export const ReferenceError = createErrorClass("ReferenceError"); +export const SyntaxError = createErrorClass("SyntaxError"); +export const TypeError = createErrorClass("TypeError"); +export const URIError = createErrorClass("URIError"); diff --git a/src/lualib/FunctionApply.ts b/src/lualib/FunctionApply.ts deleted file mode 100644 index 6776f0e02..000000000 --- a/src/lualib/FunctionApply.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare function unpack(list: T[], i?: number, j?: number): T[]; - -declare namespace table { - export function unpack(list: T[], i?: number, j?: number): T[]; -} - -type ApplyFn = (...argArray: any[]) => any; - -function __TS__FunctionApply(fn: ApplyFn, thisArg: any, argsArray?: any[]): any { - if (argsArray) { - return fn(thisArg, (unpack || table.unpack)(argsArray)); - } else { - return fn(thisArg); - } -} diff --git a/src/lualib/FunctionBind.ts b/src/lualib/FunctionBind.ts index e1e7675b5..f36e9f6e5 100644 --- a/src/lualib/FunctionBind.ts +++ b/src/lualib/FunctionBind.ts @@ -1,18 +1,10 @@ -declare function unpack(list: T[], i?: number, j?: number): T[]; - -declare namespace table { - export function insert(t: T[], pos: number, value: T): void; - - export function unpack(list: T[], i?: number, j?: number): T[]; -} - -type BindFn = (...argArray: any[]) => any; - -function __TS__FunctionBind(fn: BindFn, thisArg: any, ...boundArgs: any[]): (...args: any[]) => any { - return (...argArray: any[]) => { - for (let i = 0; i < boundArgs.length; ++i) { - table.insert(argArray, i + 1, boundArgs[i]); - } - return fn(thisArg, (unpack || table.unpack)(argArray)); +export function __TS__FunctionBind( + this: void, + fn: (this: void, ...argArray: any[]) => any, + ...boundArgs: any[] +): (...args: any[]) => any { + return (...args: any[]) => { + args.unshift(...boundArgs); + return fn(...args); }; } diff --git a/src/lualib/FunctionCall.ts b/src/lualib/FunctionCall.ts deleted file mode 100644 index fe126ccd5..000000000 --- a/src/lualib/FunctionCall.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare function unpack(list: T[], i?: number, j?: number): T[]; - -declare namespace table { - export function unpack(list: T[], i?: number, j?: number): T[]; -} - -type CallFn = (...argArray: any[]) => any; - -function __TS__FunctionCall(fn: CallFn, thisArg: any, ...args: any[]): any { - return fn(thisArg, (unpack || table.unpack)(args)); -} diff --git a/src/lualib/Generator.ts b/src/lualib/Generator.ts new file mode 100644 index 000000000..c17aab8c7 --- /dev/null +++ b/src/lualib/Generator.ts @@ -0,0 +1,29 @@ +import { __TS__CountVarargs } from "./CountVarargs"; +import { GeneratorIterator } from "./GeneratorIterator"; +import { __TS__Unpack } from "./Unpack"; + +function generatorIterator(this: GeneratorIterator) { + return this; +} + +function generatorNext(this: GeneratorIterator, ...args: any[]) { + const co = this.____coroutine; + if (coroutine.status(co) === "dead") return { done: true }; + + const [status, value] = coroutine.resume(co, ...args); + if (!status) throw value; + + return { value, done: coroutine.status(co) === "dead" }; +} + +export function __TS__Generator(this: void, fn: (this: void, ...args: any[]) => any) { + return function (this: void, ...args: any[]): GeneratorIterator { + const argsLength = __TS__CountVarargs(...args); + return { + // Using explicit this there, since we don't pass arguments after the first nil and context is likely to be nil + ____coroutine: coroutine.create(() => fn(...__TS__Unpack(args, 1, argsLength))), + [Symbol.iterator]: generatorIterator, + next: generatorNext, + }; + }; +} diff --git a/src/lualib/GeneratorIterator.d.ts b/src/lualib/GeneratorIterator.d.ts new file mode 100644 index 000000000..2509046b5 --- /dev/null +++ b/src/lualib/GeneratorIterator.d.ts @@ -0,0 +1,5 @@ +export interface GeneratorIterator { + ____coroutine: LuaThread; + [Symbol.iterator](): GeneratorIterator; + next: typeof generatorNext; +} diff --git a/src/lualib/Index.ts b/src/lualib/Index.ts deleted file mode 100644 index a5f44081f..000000000 --- a/src/lualib/Index.ts +++ /dev/null @@ -1,38 +0,0 @@ -interface LuaClass { - prototype: LuaObject; - ____super?: LuaClass; -} - -declare interface LuaObject { - constructor: LuaClass; - ____getters?: { [key: string]: (self: LuaObject) => any }; -} - -declare function rawget(obj: T, key: K): T[K]; - -function __TS__Index(classProto: LuaObject): (tbl: LuaObject, key: keyof LuaObject) => any { - return (tbl, key) => { - let proto = classProto; - while (true) { - const val = rawget(proto, key); - if (val !== null) { - return val; - } - - const getters = rawget(proto, "____getters"); - if (getters) { - const getter = getters[key]; - if (getter) { - return getter(tbl); - } - } - - const base = rawget(rawget(proto, "constructor"), "____super"); - if (!base) { - break; - } - - proto = rawget(base, "prototype"); - } - }; -} diff --git a/src/lualib/InstanceOf.ts b/src/lualib/InstanceOf.ts index c23c3604a..317c0b794 100644 --- a/src/lualib/InstanceOf.ts +++ b/src/lualib/InstanceOf.ts @@ -1,19 +1,20 @@ -interface LuaClass { - ____super?: LuaClass; -} +export function __TS__InstanceOf(this: void, obj: LuaClassInstance, classTbl: LuaClass): boolean { + if (typeof classTbl !== "object") { + throw "Right-hand side of 'instanceof' is not an object"; + } -interface LuaObject { - constructor: LuaClass; -} + if (classTbl[Symbol.hasInstance] !== undefined) { + // eslint-disable-next-line no-implicit-coercion + return !!classTbl[Symbol.hasInstance]!(obj); + } -function __TS__InstanceOf(obj: LuaObject, classTbl: LuaClass): boolean { - if (obj !== undefined) { + if (typeof obj === "object") { let luaClass = obj.constructor; while (luaClass !== undefined) { if (luaClass === classTbl) { return true; } - luaClass = luaClass.____super; + luaClass = luaClass.____super!; } } return false; diff --git a/src/lualib/InstanceOfObject.ts b/src/lualib/InstanceOfObject.ts new file mode 100644 index 000000000..28e32150e --- /dev/null +++ b/src/lualib/InstanceOfObject.ts @@ -0,0 +1,4 @@ +export function __TS__InstanceOfObject(this: void, value: unknown): boolean { + const valueType = type(value); + return valueType === "table" || valueType === "function"; +} diff --git a/src/lualib/Iterator.ts b/src/lualib/Iterator.ts index 805792052..6991ea888 100644 --- a/src/lualib/Iterator.ts +++ b/src/lualib/Iterator.ts @@ -1,11 +1,39 @@ -function __TS__Iterator(iterable: Iterable): () => T { - const iterator = iterable[Symbol.iterator](); - return () => { - const result = iterator.next(); - if (!result.done) { - return result.value; - } else { - return undefined; - } - }; +import { GeneratorIterator } from "./GeneratorIterator"; + +function iteratorGeneratorStep(this: GeneratorIterator): LuaMultiReturn<[true, any] | []> { + const co = this.____coroutine; + + const [status, value] = coroutine.resume(co); + if (!status) throw value; + + if (coroutine.status(co) === "dead") return $multi(); + return $multi(true, value); +} + +function iteratorIteratorStep(this: Iterator): LuaMultiReturn<[true, T] | []> { + const result = this.next(); + if (result.done) return $multi(); + return $multi(true, result.value); +} + +function iteratorStringStep(this: string, index: number): LuaMultiReturn<[number, string] | []> { + index += 1; + if (index > this.length) return $multi(); + return $multi(index, string.sub(this, index, index)); +} + +export function __TS__Iterator( + this: void, + iterable: string | GeneratorIterator | Iterable | readonly T[] +): LuaMultiReturn<[(...args: any[]) => [any, any] | [], ...any[]]> | LuaIterable> { + if (typeof iterable === "string") { + return $multi(iteratorStringStep, iterable, 0); + } else if ("____coroutine" in iterable) { + return $multi(iteratorGeneratorStep, iterable); + } else if (iterable[Symbol.iterator]) { + const iterator = iterable[Symbol.iterator](); + return $multi(iteratorIteratorStep, iterator); + } else { + return ipairs(iterable as readonly T[]); + } } diff --git a/src/lualib/LuaIteratorSpread.ts b/src/lualib/LuaIteratorSpread.ts new file mode 100644 index 000000000..b846bc535 --- /dev/null +++ b/src/lualib/LuaIteratorSpread.ts @@ -0,0 +1,13 @@ +export function __TS__LuaIteratorSpread( + this: (this: void, state: TState, key: TKey) => LuaMultiReturn<[TKey, TValue]>, + state: TState, + firstKey: TKey +): LuaMultiReturn> { + const results = []; + let [key, value] = this(state, firstKey); + while (key) { + results.push([key, value]); + [key, value] = this(state, key); + } + return $multi(...results) as LuaMultiReturn>; +} diff --git a/src/lualib/Map.ts b/src/lualib/Map.ts index fd55ef62f..16fa7e453 100644 --- a/src/lualib/Map.ts +++ b/src/lualib/Map.ts @@ -1,114 +1,158 @@ -/** @tupleReturn */ -declare function next(t: { [k: string]: TValue }, index?: TKey): [TKey, TValue]; +export class Map { + public static [Symbol.species] = Map; + public [Symbol.toStringTag] = "Map"; -class Map { - public size: number; + private items = new LuaTable(); + public size = 0; - private items: {[key: string]: TValue}; // Type of key is actually TKey + // Key-order variables + private firstKey: K | undefined; + private lastKey: K | undefined; + private nextKey = new LuaTable(); + private previousKey = new LuaTable(); - constructor(other: Iterable<[TKey, TValue]> | Array<[TKey, TValue]>) { - this.items = {}; - this.size = 0; + constructor(entries?: Iterable | Array) { + if (entries === undefined) return; - if (other) { - const iterable = other as Iterable<[TKey, TValue]>; - if (iterable[Symbol.iterator]) { - // Iterate manually because Map is compiled with ES5 which doesn't support Iterables in for...of - const iterator = iterable[Symbol.iterator](); - while (true) { - const result = iterator.next(); - if (result.done) { - break; - } - const value: [TKey, TValue] = result.value; // Ensures index is offset when tuple is accessed - this.set(value[0], value[1]); - } - } else { - const arr = other as Array<[TKey, TValue]>; - this.size = arr.length; - for (const kvp of arr) { - this.items[kvp[0] as any] = kvp[1]; + const iterable = entries as Iterable<[K, V]>; + if (iterable[Symbol.iterator]) { + // Iterate manually because Map is compiled with ES5 which doesn't support Iterables in for...of + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + break; } + + const value: [K, V] = result.value; // Ensures index is offset when tuple is accessed + this.set(value[0], value[1]); + } + } else { + const array = entries as Array<[K, V]>; + for (const kvp of array) { + this.set(kvp[0], kvp[1]); } } } public clear(): void { - this.items = {}; + this.items = new LuaTable(); + this.nextKey = new LuaTable(); + this.previousKey = new LuaTable(); + this.firstKey = undefined; + this.lastKey = undefined; this.size = 0; - return; } - public delete(key: TKey): boolean { + public delete(key: K): boolean { const contains = this.has(key); if (contains) { this.size--; + + // Do order bookkeeping + const next = this.nextKey.get(key); + const previous = this.previousKey.get(key); + if (next !== undefined && previous !== undefined) { + this.nextKey.set(previous, next); + this.previousKey.set(next, previous); + } else if (next !== undefined) { + this.firstKey = next; + this.previousKey.set(next, undefined!); + } else if (previous !== undefined) { + this.lastKey = previous; + this.nextKey.set(previous, undefined!); + } else { + this.firstKey = undefined; + this.lastKey = undefined; + } + + this.nextKey.set(key, undefined!); + this.previousKey.set(key, undefined!); } - this.items[key as any] = undefined; + this.items.set(key, undefined!); + return contains; } - public [Symbol.iterator](): IterableIterator<[TKey, TValue]> { - return this.entries(); + public forEach(callback: (value: V, key: K, map: Map) => any): void { + for (const key of this.keys()) { + callback(this.items.get(key), key, this); + } } - public entries(): IterableIterator<[TKey, TValue]> { - const items = this.items; - let key: TKey; - let value: TValue; - return { - [Symbol.iterator](): IterableIterator<[TKey, TValue]> { return this; }, - next(): IteratorResult<[TKey, TValue]> { - [key, value] = next(items, key); - return {done: !key, value: [key, value]}; - }, - }; + public get(key: K): V | undefined { + return this.items.get(key); } - public forEach(callback: (value: TValue, key: TKey, map: Map) => any): void { - for (const key in this.items) { - callback(this.items[key], key as any, this); - } - return; + public has(key: K): boolean { + return this.nextKey.get(key) !== undefined || this.lastKey === key; } - public get(key: TKey): TValue { - return this.items[key as any]; + public set(key: K, value: V): this { + const isNewValue = !this.has(key); + if (isNewValue) { + this.size++; + } + this.items.set(key, value); + + // Do order bookkeeping + if (this.firstKey === undefined) { + this.firstKey = key; + this.lastKey = key; + } else if (isNewValue) { + this.nextKey.set(this.lastKey!, key); + this.previousKey.set(key, this.lastKey!); + this.lastKey = key; + } + + return this; } - public has(key: TKey): boolean { - return this.items[key as any] !== undefined; + public [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); } - public keys(): IterableIterator { - const items = this.items; - let key: TKey; + public entries(): IterableIterator<[K, V]> { + const { items, nextKey } = this; + let key = this.firstKey; return { - [Symbol.iterator](): IterableIterator { return this; }, - next(): IteratorResult { - [key] = next(items, key); - return {done: !key, value: key}; + [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; }, }; } - public set(key: TKey, value: TValue): Map { - if (!this.has(key)) { - this.size++; - } - this.items[key as any] = value; - return this; + public keys(): IterableIterator { + const nextKey = this.nextKey; + let key = this.firstKey; + return { + [Symbol.iterator](): IterableIterator { + return this; + }, + next(): IteratorResult { + const result = { done: !key, value: key }; + key = nextKey.get(key!); + return result as IteratorResult; + }, + }; } - public values(): IterableIterator { - const items = this.items; - let key: TKey; - let value: TValue; + public values(): IterableIterator { + const { items, nextKey } = this; + let key = this.firstKey; return { - [Symbol.iterator](): IterableIterator { return this; }, - next(): IteratorResult { - [key, value] = next(items, key); - return {done: !key, value}; + [Symbol.iterator](): IterableIterator { + return this; + }, + next(): IteratorResult { + const result = { done: !key, value: items.get(key!) }; + key = nextKey.get(key!); + return result; }, }; } diff --git a/src/lualib/MapGroupBy.ts b/src/lualib/MapGroupBy.ts new file mode 100644 index 000000000..d3e079192 --- /dev/null +++ b/src/lualib/MapGroupBy.ts @@ -0,0 +1,22 @@ +export function __TS__MapGroupBy( + this: void, + items: Iterable, + keySelector: (item: T, index: number) => K +): Map { + const result = new Map(); + + let i = 0; + for (const item of items) { + const key = keySelector(item, i); + + if (result.has(key)) { + result.get(key)!.push(item); + } else { + result.set(key, [item]); + } + + i++; + } + + return result; +} diff --git a/src/lualib/MathAtan2.ts b/src/lualib/MathAtan2.ts new file mode 100644 index 000000000..185cfe678 --- /dev/null +++ b/src/lualib/MathAtan2.ts @@ -0,0 +1 @@ +export const __TS__MathAtan2 = math.atan2 || math.atan; diff --git a/src/lualib/MathSign.ts b/src/lualib/MathSign.ts new file mode 100644 index 000000000..629895a11 --- /dev/null +++ b/src/lualib/MathSign.ts @@ -0,0 +1,15 @@ +// https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.sign + +import { __TS__NumberIsNaN } from "./NumberIsNaN"; + +export function __TS__MathSign(this: void, val: number) { + if (__TS__NumberIsNaN(val) || val === 0) { + return val; + } + + if (val < 0) { + return -1; + } + + return 1; +} diff --git a/src/lualib/MathTrunc.ts b/src/lualib/MathTrunc.ts new file mode 100644 index 000000000..f244c2147 --- /dev/null +++ b/src/lualib/MathTrunc.ts @@ -0,0 +1,10 @@ +// https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.trunc + +import { __TS__NumberIsFinite } from "./NumberIsFinite"; +export function __TS__MathTrunc(this: void, val: number) { + if (!__TS__NumberIsFinite(val) || val === 0) { + return val; + } + + return val > 0 ? math.floor(val) : math.ceil(val); +} diff --git a/src/lualib/New.ts b/src/lualib/New.ts new file mode 100644 index 000000000..b54890917 --- /dev/null +++ b/src/lualib/New.ts @@ -0,0 +1,5 @@ +export function __TS__New(this: void, target: LuaClass, ...args: any[]): any { + const instance: any = setmetatable({}, target.prototype); + instance.____constructor(...args); + return instance; +} diff --git a/src/lualib/NewIndex.ts b/src/lualib/NewIndex.ts deleted file mode 100644 index d50d76ab7..000000000 --- a/src/lualib/NewIndex.ts +++ /dev/null @@ -1,37 +0,0 @@ -interface LuaClass { - prototype: LuaObject; - ____super?: LuaClass; -} - -declare interface LuaObject { - constructor: LuaClass; - ____setters?: { [key: string]: (self: LuaObject, val: any) => void }; -} - -declare function rawget(obj: T, key: K): T[K]; -declare function rawset(obj: T, key: K, val: T[K]): void; - -function __TS__NewIndex(classProto: LuaObject): (tbl: LuaObject, key: keyof LuaObject, val: any) => void { - return (tbl, key, val) => { - let proto = classProto; - while (true) { - const setters = rawget(proto, "____setters"); - if (setters) { - const setter = setters[key]; - if (setter) { - setter(tbl, val); - return; - } - } - - const base = rawget(rawget(proto, "constructor"), "____super"); - if (!base) { - break; - } - - proto = rawget(base, "prototype"); - } - - rawset(tbl, key, val); - }; -} diff --git a/src/lualib/Number.ts b/src/lualib/Number.ts new file mode 100644 index 000000000..b258b48f6 --- /dev/null +++ b/src/lualib/Number.ts @@ -0,0 +1,20 @@ +export function __TS__Number(this: void, value: unknown): number { + const valueType = type(value); + if (valueType === "number") { + return value as number; + } else if (valueType === "string") { + const numberValue = tonumber(value); + if (numberValue) return numberValue; + + if (value === "Infinity") return Infinity; + if (value === "-Infinity") return -Infinity; + const [stringWithoutSpaces] = string.gsub(value as string, "%s", ""); + if (stringWithoutSpaces === "") return 0; + + return NaN; + } else if (valueType === "boolean") { + return value ? 1 : 0; + } else { + return NaN; + } +} diff --git a/src/lualib/NumberIsFinite.ts b/src/lualib/NumberIsFinite.ts new file mode 100644 index 000000000..f225b8c5c --- /dev/null +++ b/src/lualib/NumberIsFinite.ts @@ -0,0 +1,3 @@ +export function __TS__NumberIsFinite(this: void, value: unknown): boolean { + return typeof value === "number" && value === value && value !== Infinity && value !== -Infinity; +} diff --git a/src/lualib/NumberIsInteger.ts b/src/lualib/NumberIsInteger.ts new file mode 100644 index 000000000..275fcae03 --- /dev/null +++ b/src/lualib/NumberIsInteger.ts @@ -0,0 +1,6 @@ +import { __TS__NumberIsFinite } from "./NumberIsFinite"; + +/// https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.isinteger +export function __TS__NumberIsInteger(this: void, value: unknown): boolean { + return __TS__NumberIsFinite(value) && math.floor(value as number) === (value as number); +} diff --git a/src/lualib/NumberIsNaN.ts b/src/lualib/NumberIsNaN.ts new file mode 100644 index 000000000..e57631cc1 --- /dev/null +++ b/src/lualib/NumberIsNaN.ts @@ -0,0 +1,3 @@ +export function __TS__NumberIsNaN(this: void, value: unknown): boolean { + return value !== value; +} diff --git a/src/lualib/NumberToFixed.ts b/src/lualib/NumberToFixed.ts new file mode 100644 index 000000000..355da13f0 --- /dev/null +++ b/src/lualib/NumberToFixed.ts @@ -0,0 +1,14 @@ +/// https://www.ecma-international.org/ecma-262/10.0/index.html#sec-number.prototype.tofixed +export function __TS__NumberToFixed(this: number, fractionDigits?: number): string { + if (Math.abs(this) >= 1e21 || this !== this) { + return this.toString(); + } + const f = Math.floor(fractionDigits ?? 0); + // reduced to 99 as string.format only supports 2-digit numbers + if (f < 0 || f > 99) { + throw "toFixed() digits argument must be between 0 and 99"; + } + // throws "invalid format (width or precision too long)" if strlen > 99 + // if (f < 80) return fmt; else try {return fmt} catch(_) { throw "toFixed() digits argument..." } + return string.format(`%.${f}f`, this); +} diff --git a/src/lualib/NumberToString.ts b/src/lualib/NumberToString.ts new file mode 100644 index 000000000..6cdbaadc3 --- /dev/null +++ b/src/lualib/NumberToString.ts @@ -0,0 +1,49 @@ +import { __TS__MathModf } from "./MathModf"; + +const radixChars = "0123456789abcdefghijklmnopqrstuvwxyz"; + +// https://www.ecma-international.org/ecma-262/10.0/index.html#sec-number.prototype.tostring +export function __TS__NumberToString(this: number, radix?: number): string { + if (radix === undefined || radix === 10 || this === Infinity || this === -Infinity || this !== this) { + return this.toString(); + } + + radix = Math.floor(radix); + if (radix < 2 || radix > 36) { + throw "toString() radix argument must be between 2 and 36"; + } + + let [integer, fraction] = __TS__MathModf(Math.abs(this)); + + let result = ""; + if (radix === 8) { + result = string.format("%o", integer); + } else if (radix === 16) { + result = string.format("%x", integer); + } else { + do { + result = radixChars[integer % radix] + result; + integer = Math.floor(integer / radix); + } while (integer !== 0); + } + + // https://github.com/v8/v8/blob/f78e8d43c224847fa56b3220a90be250fc0f0d6e/src/numbers/conversions.cc#L1221 + if (fraction !== 0) { + result += "."; + let delta = 1e-16; + do { + fraction *= radix; + delta *= radix; + const digit = Math.floor(fraction); + result += radixChars[digit]; + fraction -= digit; + // TODO: Round to even + } while (fraction >= delta); + } + + if (this < 0) { + result = "-" + result; + } + + return result; +} diff --git a/src/lualib/ObjectAssign.ts b/src/lualib/ObjectAssign.ts index ff0b3fb41..a7f691433 100644 --- a/src/lualib/ObjectAssign.ts +++ b/src/lualib/ObjectAssign.ts @@ -1,14 +1,11 @@ // https://tc39.github.io/ecma262/#sec-object.assign -function __TS__ObjectAssign(to: T, ...sources: object[]): T { - if (to === undefined) { - return to; - } - - for (const source of sources) { +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) { - to[key] = source[key]; + target[key] = source[key]; } } - return to; + return target; } diff --git a/src/lualib/ObjectDefineProperty.ts b/src/lualib/ObjectDefineProperty.ts new file mode 100644 index 000000000..c0dc40a1f --- /dev/null +++ b/src/lualib/ObjectDefineProperty.ts @@ -0,0 +1,37 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty + +import { __TS__SetDescriptor } from "./SetDescriptor"; + +export function __TS__ObjectDefineProperty( + this: void, + target: T, + key: any, + desc: PropertyDescriptor +): T { + const luaKey = typeof key === "number" ? key + 1 : key; + const value = rawget(target, luaKey); + + const hasGetterOrSetter = desc.get !== undefined || desc.set !== undefined; + + let descriptor: PropertyDescriptor; + if (hasGetterOrSetter) { + if (value !== undefined) { + throw `Cannot redefine property: ${key}`; + } + + descriptor = desc; + } else { + const valueExists = value !== undefined; + descriptor = { + set: desc.set, + get: desc.get, + configurable: desc.configurable ?? valueExists, + enumerable: desc.enumerable ?? valueExists, + writable: desc.writable ?? valueExists, + value: desc.value !== undefined ? desc.value : value, + }; + } + + __TS__SetDescriptor(target, luaKey, descriptor); + return target; +} diff --git a/src/lualib/ObjectEntries.ts b/src/lualib/ObjectEntries.ts index 51f32cf10..20fde96ad 100644 --- a/src/lualib/ObjectEntries.ts +++ b/src/lualib/ObjectEntries.ts @@ -1,7 +1,12 @@ -function __TS__ObjectEntries(obj: any): Array { - const result = []; +export function __TS__ObjectEntries( + this: void, + obj: Record +): Array<[TKey, TValue]> { + const result: Array<[TKey, TValue]> = []; + let len = 0; for (const key in obj) { - result[result.length] = [key, obj[key]]; + len++; + result[len - 1] = [key, obj[key]]; } return result; } diff --git a/src/lualib/ObjectFromEntries.ts b/src/lualib/ObjectFromEntries.ts new file mode 100644 index 000000000..88c224cf7 --- /dev/null +++ b/src/lualib/ObjectFromEntries.ts @@ -0,0 +1,24 @@ +export function __TS__ObjectFromEntries( + this: void, + entries: ReadonlyArray<[string, T]> | Iterable<[string, T]> +): Record { + const obj: Record = {}; + + const iterable = entries as Iterable<[string, T]>; + if (iterable[Symbol.iterator]) { + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) break; + + const value: [string, T] = result.value; + obj[value[0]] = value[1]; + } + } else { + for (const entry of entries as ReadonlyArray<[string, T]>) { + obj[entry[0]] = entry[1]; + } + } + + return obj; +} diff --git a/src/lualib/ObjectGetOwnPropertyDescriptor.ts b/src/lualib/ObjectGetOwnPropertyDescriptor.ts new file mode 100644 index 000000000..975de6712 --- /dev/null +++ b/src/lualib/ObjectGetOwnPropertyDescriptor.ts @@ -0,0 +1,10 @@ +export function __TS__ObjectGetOwnPropertyDescriptor( + this: void, + object: any, + key: any +): PropertyDescriptor | undefined { + const metatable = getmetatable(object); + if (!metatable) return; + if (!rawget(metatable, "_descriptors")) return; + return rawget(metatable, "_descriptors")![key]; +} diff --git a/src/lualib/ObjectGetOwnPropertyDescriptors.ts b/src/lualib/ObjectGetOwnPropertyDescriptors.ts new file mode 100644 index 000000000..69d23af06 --- /dev/null +++ b/src/lualib/ObjectGetOwnPropertyDescriptors.ts @@ -0,0 +1,8 @@ +export function __TS__ObjectGetOwnPropertyDescriptors( + this: void, + object: any +): Record { + const metatable = getmetatable(object); + if (!metatable) return {}; + return rawget(metatable, "_descriptors") ?? {}; +} diff --git a/src/lualib/ObjectGroupBy.ts b/src/lualib/ObjectGroupBy.ts new file mode 100644 index 000000000..6328ad2aa --- /dev/null +++ b/src/lualib/ObjectGroupBy.ts @@ -0,0 +1,22 @@ +export function __TS__ObjectGroupBy( + this: void, + items: Iterable, + keySelector: (item: T, index: number) => K +): Partial> { + const result: Partial> = {}; + + let i = 0; + for (const item of items) { + const key = keySelector(item, i); + + if (key in result) { + result[key]!.push(item); + } else { + result[key] = [item]; + } + + i++; + } + + return result; +} diff --git a/src/lualib/ObjectKeys.ts b/src/lualib/ObjectKeys.ts index 5358acad8..c7a0c0208 100644 --- a/src/lualib/ObjectKeys.ts +++ b/src/lualib/ObjectKeys.ts @@ -1,7 +1,9 @@ -function __TS__ObjectKeys(obj: any): Array { +export function __TS__ObjectKeys(this: void, obj: any): Array { const result = []; + let len = 0; for (const key in obj) { - result[result.length] = key; + len++; + result[len - 1] = key; } return result; } diff --git a/src/lualib/ObjectRest.ts b/src/lualib/ObjectRest.ts new file mode 100644 index 000000000..276135ce5 --- /dev/null +++ b/src/lualib/ObjectRest.ts @@ -0,0 +1,14 @@ +export function __TS__ObjectRest( + this: void, + target: Record, + usedProperties: Partial> +): Partial> { + const result: Partial> = {}; + for (const property in target) { + if (!usedProperties[property]) { + result[property] = target[property]; + } + } + + return result; +} diff --git a/src/lualib/ObjectValues.ts b/src/lualib/ObjectValues.ts index c7a17377a..925a61ab3 100644 --- a/src/lualib/ObjectValues.ts +++ b/src/lualib/ObjectValues.ts @@ -1,7 +1,9 @@ -function __TS__ObjectValues(obj: any): Array { +export function __TS__ObjectValues(this: void, obj: any): Array { const result = []; + let len = 0; for (const key in obj) { - result[result.length] = obj[key]; + len++; + result[len - 1] = obj[key]; } return result; } diff --git a/src/lualib/ParseFloat.ts b/src/lualib/ParseFloat.ts new file mode 100644 index 000000000..7720acb6b --- /dev/null +++ b/src/lualib/ParseFloat.ts @@ -0,0 +1,13 @@ +import { __TS__Match } from "./Match"; + +export function __TS__ParseFloat(this: void, numberString: string): number { + // Check if string is infinity + const [infinityMatch] = __TS__Match(numberString, "^%s*(-?Infinity)"); + if (infinityMatch !== undefined) { + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + return infinityMatch[0] === "-" ? -Infinity : Infinity; + } + + const number = tonumber(__TS__Match(numberString, "^%s*(-?%d+%.?%d*)")[0]); + return number ?? NaN; +} diff --git a/src/lualib/ParseInt.ts b/src/lualib/ParseInt.ts new file mode 100644 index 000000000..c27d62662 --- /dev/null +++ b/src/lualib/ParseInt.ts @@ -0,0 +1,41 @@ +import { __TS__Match } from "./Match"; + +const parseIntBasePattern = "0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTvVwWxXyYzZ"; + +export function __TS__ParseInt(this: void, numberString: string, base?: number): number { + // Check which base to use if none specified + if (base === undefined) { + base = 10; + const [hexMatch] = __TS__Match(numberString, "^%s*-?0[xX]"); + if (hexMatch !== undefined) { + base = 16; + numberString = __TS__Match(hexMatch, "-")[0] + ? "-" + numberString.substring(hexMatch.length) + : numberString.substring(hexMatch.length); + } + } + + // Check if base is in bounds + if (base < 2 || base > 36) { + return NaN; + } + + // Calculate string match pattern to use + const allowedDigits = + base <= 10 ? parseIntBasePattern.substring(0, base) : parseIntBasePattern.substring(0, 10 + 2 * (base - 10)); + const pattern = `^%s*(-?[${allowedDigits}]*)`; + + // Try to parse with Lua tonumber + const number = tonumber(__TS__Match(numberString, pattern)[0], base); + + if (number === undefined) { + return NaN; + } + + // Lua uses a different floor convention for negative numbers than JS + if (number >= 0) { + return math.floor(number); + } else { + return math.ceil(number); + } +} diff --git a/src/lualib/Promise.ts b/src/lualib/Promise.ts new file mode 100644 index 000000000..c5b6aa2a5 --- /dev/null +++ b/src/lualib/Promise.ts @@ -0,0 +1,245 @@ +/* eslint-disable @typescript-eslint/promise-function-async */ + +// Promises implemented based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +// and https://promisesaplus.com/ + +export const enum PromiseState { + Pending, + Fulfilled, + Rejected, +} + +type PromiseExecutor = ConstructorParameters>[0]; +type PromiseResolve = Parameters>[0]; +type PromiseReject = Parameters>[1]; +type PromiseResolveCallback = (value: TValue) => TResult | PromiseLike; +type PromiseRejectCallback = (reason: any) => TResult | PromiseLike; + +function makeDeferredPromiseFactory(this: void) { + let resolve: PromiseResolve; + let reject: PromiseReject; + const executor: PromiseExecutor = (res, rej) => { + resolve = res; + reject = rej; + }; + return function (this: void) { + const promise = new Promise(executor); + return $multi(promise, resolve, reject); + }; +} + +const makeDeferredPromise = makeDeferredPromiseFactory(); + +function isPromiseLike(this: void, value: unknown): value is PromiseLike { + return value instanceof __TS__Promise; +} + +function doNothing(): void {} + +const pcall = _G.pcall; + +export class __TS__Promise implements Promise { + public state = PromiseState.Pending; + public value?: T; + public rejectionReason?: any; + + private fulfilledCallbacks: Array> = []; + private rejectedCallbacks: PromiseReject[] = []; + private finallyCallbacks: Array<() => void> = []; + + // @ts-ignore + public [Symbol.toStringTag]: string; // Required to implement interface, no output Lua + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve + public static resolve(this: void, value: T | PromiseLike): __TS__Promise> { + if (value instanceof __TS__Promise) { + return value; + } + // Create and return a promise instance that is already resolved + const promise = new __TS__Promise>(doNothing); + promise.state = PromiseState.Fulfilled; + promise.value = value as Awaited; + return promise; + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject + public static reject(this: void, reason?: any): __TS__Promise { + // Create and return a promise instance that is already rejected + const promise = new __TS__Promise(doNothing); + promise.state = PromiseState.Rejected; + promise.rejectionReason = reason; + return promise; + } + + constructor(executor: PromiseExecutor) { + // Avoid unnecessary local functions allocations by using `pcall` explicitly + const [success, error] = pcall( + executor, + undefined, + v => this.resolve(v), + err => this.reject(err) + ); + if (!success) { + // When a promise executor throws, the promise should be rejected with the thrown object as reason + this.reject(error); + } + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + public then( + onFulfilled?: PromiseResolveCallback, + onRejected?: PromiseRejectCallback + ): Promise { + const [promise, resolve, reject] = makeDeferredPromise(); + + this.addCallbacks( + // We always want to resolve our child promise if this promise is resolved, even if we have no handler + onFulfilled ? this.createPromiseResolvingCallback(onFulfilled, resolve, reject) : resolve, + // We always want to reject our child promise if this promise is rejected, even if we have no handler + onRejected ? this.createPromiseResolvingCallback(onRejected, resolve, reject) : reject + ); + + return promise as Promise; + } + + // Both callbacks should never throw! + public addCallbacks(fulfilledCallback: (value: T) => void, rejectedCallback: (rejectionReason: any) => void): void { + if (this.state === PromiseState.Fulfilled) { + // If promise already resolved, immediately call callback. We don't even need to store rejected callback + // Tail call return is important! + return fulfilledCallback(this.value!); + } + if (this.state === PromiseState.Rejected) { + // Similar thing + return rejectedCallback(this.rejectionReason); + } + + this.fulfilledCallbacks.push(fulfilledCallback as any); + this.rejectedCallbacks.push(rejectedCallback); + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch + public catch(onRejected?: (reason: any) => TResult | PromiseLike): Promise { + return this.then(undefined, onRejected); + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally + public finally(onFinally?: () => void): Promise { + if (onFinally) { + this.finallyCallbacks.push(onFinally); + + if (this.state !== PromiseState.Pending) { + // If promise already resolved or rejected, immediately fire finally callback + onFinally(); + } + } + return this; + } + + private resolve(value: T | PromiseLike): void { + if (isPromiseLike(value)) { + // Tail call return is important! + return (value as __TS__Promise).addCallbacks( + v => this.resolve(v), + err => this.reject(err) + ); + } + + // Resolve this promise, if it is still pending. This function is passed to the constructor function. + if (this.state === PromiseState.Pending) { + this.state = PromiseState.Fulfilled; + this.value = value; + + // Tail call return is important! + return this.invokeCallbacks(this.fulfilledCallbacks, value); + } + } + + private reject(reason: any): void { + // Reject this promise, if it is still pending. This function is passed to the constructor function. + if (this.state === PromiseState.Pending) { + this.state = PromiseState.Rejected; + this.rejectionReason = reason; + + // Tail call return is important! + return this.invokeCallbacks(this.rejectedCallbacks, reason); + } + } + + 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](); + } + } + + private createPromiseResolvingCallback( + f: PromiseResolveCallback | PromiseRejectCallback, + resolve: (data: TResult1 | TResult2) => void, + reject: (reason: any) => void + ) { + return (value: T): void => { + const [success, resultOrError] = pcall< + undefined, + [T], + TResult1 | PromiseLike | TResult2 | PromiseLike + >(f, undefined, value); + if (!success) { + // Tail call return is important! + return reject(resultOrError); + } + // Tail call return is important! + return this.handleCallbackValue(resultOrError, resolve, reject); + }; + } + + private handleCallbackValue( + value: TResult | PromiseLike, + resolve: (data: TResult1 | TResult2) => void, + reject: (reason: any) => void + ): void { + if (isPromiseLike(value)) { + const nextpromise = value as __TS__Promise; + if (nextpromise.state === PromiseState.Fulfilled) { + // If a handler function returns an already fulfilled promise, + // the promise returned by then gets fulfilled with that promise's value. + // Tail call return is important! + return resolve(nextpromise.value!); + } else if (nextpromise.state === PromiseState.Rejected) { + // If a handler function returns an already rejected promise, + // the promise returned by then gets fulfilled with that promise's value. + // Tail call return is important! + return reject(nextpromise.rejectionReason); + } else { + // If a handler function returns another pending promise object, the resolution/rejection + // of the promise returned by then will be subsequent to the resolution/rejection of + // the promise returned by the handler. + // We cannot use `then` because we need to do tail call, and `then` returns a Promise. + // `resolve` and `reject` should never throw. + return nextpromise.addCallbacks(resolve, reject); + } + } else { + // If a handler returns a value, the promise returned by then gets resolved with the returned value as its value + // If a handler doesn't return anything, the promise returned by then gets resolved with undefined + // Tail call return is important! + return resolve(value); + } + } +} diff --git a/src/lualib/PromiseAll.ts b/src/lualib/PromiseAll.ts new file mode 100644 index 000000000..c9263b645 --- /dev/null +++ b/src/lualib/PromiseAll.ts @@ -0,0 +1,56 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all +import { __TS__Promise, PromiseState } from "./Promise"; + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export function __TS__PromiseAll(this: void, iterable: Iterable>): Promise { + const results: T[] = []; + + const toResolve = new LuaTable>(); + let numToResolve = 0; + + let i = 0; + for (const item of iterable) { + if (item instanceof __TS__Promise) { + if (item.state === PromiseState.Fulfilled) { + // If value is a resolved promise, add its value to our results array + results[i] = item.value; + } else if (item.state === PromiseState.Rejected) { + // If value is a rejected promise, return a rejected promise with the rejection reason + return Promise.reject(item.rejectionReason); + } else { + // If value is a pending promise, add it to the list of pending promises + numToResolve++; + toResolve.set(i, item); + } + } else { + // If value is not a promise, add it to the results array + results[i] = item as T; + } + i++; + } + + // If there are no remaining pending promises, return a resolved promise with the results + if (numToResolve === 0) { + return Promise.resolve(results); + } + + return new Promise((resolve, reject) => { + for (const [index, promise] of pairs(toResolve)) { + promise.then( + data => { + // When resolved, store value in results array + results[index] = data; + numToResolve--; + if (numToResolve === 0) { + // If there are no more promises to resolve, resolve with our filled results array + resolve(results); + } + }, + reason => { + // When rejected, immediately reject the returned promise + reject(reason); + } + ); + } + }); +} diff --git a/src/lualib/PromiseAllSettled.ts b/src/lualib/PromiseAllSettled.ts new file mode 100644 index 000000000..4bcd13123 --- /dev/null +++ b/src/lualib/PromiseAllSettled.ts @@ -0,0 +1,64 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled +import { __TS__Promise, PromiseState } from "./Promise"; + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export function __TS__PromiseAllSettled( + this: void, + iterable: Iterable +): Promise ? U : T>>> { + const results: Array ? U : T>> = []; + + const toResolve = new LuaTable>(); + let numToResolve = 0; + + let i = 0; + for (const item of iterable) { + if (item instanceof __TS__Promise) { + if (item.state === PromiseState.Fulfilled) { + // If value is a resolved promise, add a fulfilled PromiseSettledResult + results[i] = { status: "fulfilled", value: item.value }; + } else if (item.state === PromiseState.Rejected) { + // If value is a rejected promise, add a rejected PromiseSettledResult + results[i] = { status: "rejected", reason: item.rejectionReason }; + } else { + // If value is a pending promise, add it to the list of pending promises + numToResolve++; + toResolve.set(i, item); + } + } else { + // If value is not a promise, add it to the results as fulfilled PromiseSettledResult + results[i] = { status: "fulfilled", value: item as any }; + } + i++; + } + + // If there are no remaining pending promises, return a resolved promise with the results + if (numToResolve === 0) { + return Promise.resolve(results); + } + + return new Promise(resolve => { + for (const [index, promise] of pairs(toResolve)) { + promise.then( + data => { + // When resolved, add a fulfilled PromiseSettledResult + results[index] = { status: "fulfilled", value: data as any }; + numToResolve--; + if (numToResolve === 0) { + // If there are no more promises to resolve, resolve with our filled results array + resolve(results); + } + }, + reason => { + // When resolved, add a rejected PromiseSettledResult + results[index] = { status: "rejected", reason }; + numToResolve--; + if (numToResolve === 0) { + // If there are no more promises to resolve, resolve with our filled results array + resolve(results); + } + } + ); + } + }); +} diff --git a/src/lualib/PromiseAny.ts b/src/lualib/PromiseAny.ts new file mode 100644 index 000000000..17c77f273 --- /dev/null +++ b/src/lualib/PromiseAny.ts @@ -0,0 +1,57 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any +import { __TS__Promise, PromiseState } from "./Promise"; + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export function __TS__PromiseAny(this: void, iterable: Iterable>): Promise { + const rejections: string[] = []; + const pending: Array> = []; + + for (const item of iterable) { + if (item instanceof __TS__Promise) { + if (item.state === PromiseState.Fulfilled) { + // If value is a resolved promise, return a new resolved promise with its value + return Promise.resolve(item.value); + } else if (item.state === PromiseState.Rejected) { + // If value is a rejected promise, add its value to our list of rejections + rejections.push(item.rejectionReason); + } else { + // If value is a pending promise, add it to the list of pending promises + pending.push(item); + } + } else { + // If value is not a promise, return a resolved promise with it as its value + return Promise.resolve(item); + } + } + + // If we have not returned yet and there are no pending promises, reject + if (pending.length === 0) { + return Promise.reject("No promises to resolve with .any()"); + } + + let numResolved = 0; + + return new Promise((resolve, reject) => { + for (const promise of pending) { + promise.then( + data => { + // If any pending promise resolves, resolve this promise with its data + resolve(data); + }, + reason => { + // If a pending promise rejects, add its rejection to our rejection list + rejections.push(reason); + numResolved++; + if (numResolved === pending.length) { + // If there are no more pending promises, reject with the list of rejections + reject({ + name: "AggregateError", + message: "All Promises rejected", + errors: rejections, + }); + } + } + ); + } + }); +} diff --git a/src/lualib/PromiseRace.ts b/src/lualib/PromiseRace.ts new file mode 100644 index 000000000..238aaf33b --- /dev/null +++ b/src/lualib/PromiseRace.ts @@ -0,0 +1,36 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race +import { PromiseState, __TS__Promise } from "./Promise"; + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export function __TS__PromiseRace(this: void, iterable: Iterable>): Promise { + const pending: Array> = []; + + for (const item of iterable) { + if (item instanceof __TS__Promise) { + if (item.state === PromiseState.Fulfilled) { + // If value is a fulfilled promise, return a resolved promise with its value + return Promise.resolve(item.value); + } else if (item.state === PromiseState.Rejected) { + // If value is a rejected promise, return rejected promise with its value + return Promise.reject(item.rejectionReason); + } else { + // If value is a pending promise, add it to the list of pending promises + pending.push(item); + } + } else { + // If value is not a promise, return a promise resolved with it as its value + return Promise.resolve(item); + } + } + + // If not yet returned, wait for any pending promise to resolve or reject. + // If there are no pending promise, this promise will be pending forever as per specification. + return new Promise((resolve, reject) => { + for (const promise of pending) { + promise.then( + value => resolve(value), + reason => reject(reason) + ); + } + }); +} diff --git a/src/lualib/Set.ts b/src/lualib/Set.ts index 2b06ee1d4..b46555edf 100644 --- a/src/lualib/Set.ts +++ b/src/lualib/Set.ts @@ -1,108 +1,234 @@ -/** @tupleReturn */ -declare function next(t: { [k: string]: TValue }, index?: TKey): [TKey, TValue]; +export class Set { + public static [Symbol.species] = Set; + public [Symbol.toStringTag] = "Set"; -class Set { - public size: number; + public size = 0; - private items: {[key: string]: boolean}; // Key type is actually TValue + private firstKey: T | undefined; + private lastKey: T | undefined; + private nextKey = new LuaTable(); + private previousKey = new LuaTable(); - constructor(other: Iterable | TValue[]) { - this.items = {}; - this.size = 0; + constructor(values?: Iterable | T[]) { + if (values === undefined) return; - if (other) { - const iterable = other as Iterable; - if (iterable[Symbol.iterator]) { - // Iterate manually because Set is compiled with ES5 which doesn't support Iterables in for...of - const iterator = iterable[Symbol.iterator](); - while (true) { - const result = iterator.next(); - if (result.done) { - break; - } - this.add(result.value); - } - } else { - const arr = other as TValue[]; - this.size = arr.length; - for (const value of arr) { - this.items[value as any] = true; + const iterable = values as Iterable; + if (iterable[Symbol.iterator]) { + // Iterate manually because Set is compiled with ES5 which doesn't support Iterables in for...of + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + break; } + this.add(result.value); + } + } else { + const array = values as T[]; + for (const value of array) { + this.add(value); } } } - public add(value: TValue): Set { - if (!this.has(value)) { + public add(value: T): Set { + const isNewValue = !this.has(value); + if (isNewValue) { this.size++; } - this.items[value as any] = true; + + // Do order bookkeeping + if (this.firstKey === undefined) { + this.firstKey = value; + this.lastKey = value; + } else if (isNewValue) { + this.nextKey.set(this.lastKey!, value); + this.previousKey.set(value, this.lastKey!); + this.lastKey = value; + } + return this; } public clear(): void { - this.items = {}; + this.nextKey = new LuaTable(); + this.previousKey = new LuaTable(); + this.firstKey = undefined; + this.lastKey = undefined; this.size = 0; - return; } - public delete(value: TValue): boolean { + public delete(value: T): boolean { const contains = this.has(value); if (contains) { this.size--; + + // Do order bookkeeping + const next = this.nextKey.get(value); + const previous = this.previousKey.get(value); + if (next !== undefined && previous !== undefined) { + this.nextKey.set(previous, next); + this.previousKey.set(next, previous); + } else if (next !== undefined) { + this.firstKey = next; + this.previousKey.set(next, undefined!); + } else if (previous !== undefined) { + this.lastKey = previous; + this.nextKey.set(previous, undefined!); + } else { + this.firstKey = undefined; + this.lastKey = undefined; + } + + this.nextKey.set(value, undefined!); + this.previousKey.set(value, undefined!); } - this.items[value as any] = undefined; + return contains; } - public [Symbol.iterator](): IterableIterator { - return this.values(); + public forEach(callback: (value: T, key: T, set: Set) => any): void { + for (const key of this.keys()) { + callback(key, key, this); + } } - public entries(): IterableIterator<[TValue, TValue]> { - const items = this.items; - let key: TValue; - return { - [Symbol.iterator](): IterableIterator<[TValue, TValue]> { return this; }, - next(): IteratorResult<[TValue, TValue]> { - [key] = next(items, key); - return {done: !key, value: [key, key]}; - }, - }; + public has(value: T): boolean { + return this.nextKey.get(value) !== undefined || this.lastKey === value; } - public forEach(callback: (value: TValue, key: TValue, set: Set) => any): void { - for (const key in this.items) { - callback(key as any, key as any, this); - } - return; + public [Symbol.iterator](): IterableIterator { + return this.values(); } - public has(value: TValue): boolean { - return this.items[value as any] === true; + public entries(): IterableIterator<[T, T]> { + const nextKey = this.nextKey; + let key: T = this.firstKey!; + 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; + }, + }; } - public keys(): IterableIterator { - const items = this.items; - let key: TValue; + public keys(): IterableIterator { + const nextKey = this.nextKey; + let key: T = this.firstKey!; return { - [Symbol.iterator](): IterableIterator { return this; }, - next(): IteratorResult { - [key] = next(items, key); - return {done: !key, value: key}; + [Symbol.iterator](): IterableIterator { + return this; + }, + next(): IteratorResult { + const result = { done: !key, value: key }; + key = nextKey.get(key); + return result; }, }; } - public values(): IterableIterator { - const items = this.items; - let key: TValue; + public values(): IterableIterator { + const nextKey = this.nextKey; + let key: T = this.firstKey!; return { - [Symbol.iterator](): IterableIterator { return this; }, - next(): IteratorResult { - [key] = next(items, key); - return {done: !key, value: key}; + [Symbol.iterator](): IterableIterator { + return this; + }, + next(): IteratorResult { + const result = { done: !key, value: key }; + key = nextKey.get(key); + return result; }, }; } + + /** + * @returns a new Set containing all the elements in this Set and also all the elements in the argument. + */ + public union(other: ReadonlySet): Set { + const result = new Set(this); + for (const item of other) { + result.add(item); + } + return result; + } + + /** + * @returns a new Set containing all the elements which are both in this Set and in the argument. + */ + public intersection(other: ReadonlySet) { + const result = new Set(); + for (const item of this) { + if (other.has(item)) { + result.add(item); + } + } + return result; + } + + /** + * @returns a new Set containing all the elements in this Set which are not also in the argument. + */ + public difference(other: ReadonlySet): Set { + const result = new Set(this); + for (const item of other) { + result.delete(item); + } + return result; + } + + /** + * @returns a new Set containing all the elements which are in either this Set or in the argument, but not in both. + */ + public symmetricDifference(other: ReadonlySet): Set { + const result = new Set(this); + for (const item of other) { + if (this.has(item)) { + result.delete(item); + } else { + result.add(item); + } + } + return result; + } + + /** + * @returns a boolean indicating whether all the elements in this Set are also in the argument. + */ + public isSubsetOf(other: ReadonlySet): boolean { + for (const item of this) { + if (!other.has(item)) { + return false; + } + } + return true; + } + + /** + * @returns a boolean indicating whether all the elements in the argument are also in this Set. + */ + public isSupersetOf(other: ReadonlySet): boolean { + for (const item of other) { + if (!this.has(item as T)) { + return false; + } + } + return true; + } + + /** + * @returns a boolean indicating whether this Set has no elements in common with the argument. + */ + public isDisjointFrom(other: ReadonlySetLike): boolean { + for (const item of this) { + if (other.has(item)) { + return false; + } + } + return true; + } } diff --git a/src/lualib/SetDescriptor.ts b/src/lualib/SetDescriptor.ts new file mode 100644 index 000000000..4339c8cc1 --- /dev/null +++ b/src/lualib/SetDescriptor.ts @@ -0,0 +1,36 @@ +import { __TS__CloneDescriptor } from "./CloneDescriptor"; +import { __TS__DescriptorGet } from "./DescriptorGet"; +import { __TS__DescriptorSet } from "./DescriptorSet"; + +const getmetatable = _G.getmetatable; + +function descriptorIndex(this: any, key: string): void { + return __TS__DescriptorGet.call(this, getmetatable(this), key); +} + +function descriptorNewIndex(this: any, key: string, value: any): void { + return __TS__DescriptorSet.call(this, getmetatable(this), key, value); +} + +// It's also used directly in class transform to add descriptors to the prototype +export function __TS__SetDescriptor( + this: void, + target: any, + key: any, + desc: PropertyDescriptor, + isPrototype = false +): void { + let metatable = isPrototype ? target : getmetatable(target); + if (!metatable) { + metatable = {}; + setmetatable(target, metatable); + } + + const value = rawget(target, key); + if (value !== undefined) rawset(target, key, undefined); + + if (!rawget(metatable, "_descriptors")) metatable._descriptors = {}; + metatable._descriptors[key] = __TS__CloneDescriptor(desc); + metatable.__index = descriptorIndex; + metatable.__newindex = descriptorNewIndex; +} diff --git a/src/lualib/SourceMapTraceBack.ts b/src/lualib/SourceMapTraceBack.ts new file mode 100644 index 000000000..fe59a0f05 --- /dev/null +++ b/src/lualib/SourceMapTraceBack.ts @@ -0,0 +1,75 @@ +// TODO: In the future, change this to __TS__RegisterFileInfo and provide tstl interface to +// get some metadata about transpilation. + +import { __TS__Match } from "./Match"; + +interface SourceMap { + [line: string]: number | { line: number; file: string }; +} + +declare global { + function __TS__originalTraceback(this: void, thread?: LuaThread, message?: string, level?: number): void; + var __TS__sourcemap: Record; +} + +export function __TS__SourceMapTraceBack(this: void, fileName: string, sourceMap: SourceMap): void { + globalThis.__TS__sourcemap = globalThis.__TS__sourcemap || {}; + globalThis.__TS__sourcemap[fileName] = sourceMap; + + if (globalThis.__TS__originalTraceback === undefined) { + const originalTraceback = debug.traceback; + globalThis.__TS__originalTraceback = originalTraceback as typeof __TS__originalTraceback; + debug.traceback = ((thread?: LuaThread, message?: string, level?: number) => { + let trace: string; + if (thread === undefined && message === undefined && level === undefined) { + trace = originalTraceback(); + } else if (_VERSION.includes("Lua 5.0")) { + trace = originalTraceback(`[Level ${level}] ${message}`); + } else { + // @ts-ignore Fails when compiled with Lua 5.0 types + trace = originalTraceback(thread, message, level); + } + + if (typeof trace !== "string") { + return trace; + } + + const replacer = (file: string, srcFile: string, line: string) => { + const fileSourceMap: SourceMap = globalThis.__TS__sourcemap[file]; + if (fileSourceMap !== undefined && fileSourceMap[line] !== undefined) { + const data = fileSourceMap[line]; + if (typeof data === "number") { + return `${srcFile}:${data}`; + } + + return `${data.file}:${data.line}`; + } + + return `${file}:${line}`; + }; + + let [result] = string.gsub(trace, "(%S+)%.lua:(%d+)", (file, line) => + replacer(`${file}.lua`, `${file}.ts`, line) + ); + + const stringReplacer = (file: string, line: string) => { + const fileSourceMap: SourceMap = globalThis.__TS__sourcemap[file]; + if (fileSourceMap !== undefined && fileSourceMap[line] !== undefined) { + const chunkName = __TS__Match(file, '%[string "([^"]+)"%]')[0]; + const [sourceName] = string.gsub(chunkName, ".lua$", ".ts"); + const data = fileSourceMap[line]; + if (typeof data === "number") { + return `${sourceName}:${data}`; + } + + return `${data.file}:${data.line}`; + } + + return `${file}:${line}`; + }; + [result] = string.gsub(result, '(%[string "[^"]+"%]):(%d+)', (file, line) => stringReplacer(file, line)); + + return result; + }) as typeof debug.traceback; + } +} diff --git a/src/lualib/SparseArray.d.ts b/src/lualib/SparseArray.d.ts new file mode 100644 index 000000000..768d93d7f --- /dev/null +++ b/src/lualib/SparseArray.d.ts @@ -0,0 +1 @@ +export type __TS__SparseArray = T[] & { sparseLength: number }; diff --git a/src/lualib/SparseArrayNew.ts b/src/lualib/SparseArrayNew.ts new file mode 100644 index 000000000..c6877b9c2 --- /dev/null +++ b/src/lualib/SparseArrayNew.ts @@ -0,0 +1,9 @@ +import { __TS__CountVarargs } from "./CountVarargs"; +import { __TS__SparseArray } from "./SparseArray"; + +export function __TS__SparseArrayNew(this: void, ...args: T[]): __TS__SparseArray { + const sparseArray = [...args] as __TS__SparseArray; + // Note that we're depending on vararg optimization to occur here. + sparseArray.sparseLength = __TS__CountVarargs(...args); + return sparseArray; +} diff --git a/src/lualib/SparseArrayPush.ts b/src/lualib/SparseArrayPush.ts new file mode 100644 index 000000000..253d6e223 --- /dev/null +++ b/src/lualib/SparseArrayPush.ts @@ -0,0 +1,11 @@ +import { __TS__CountVarargs } from "./CountVarargs"; +import { __TS__SparseArray } from "./SparseArray"; + +export function __TS__SparseArrayPush(this: void, sparseArray: __TS__SparseArray, ...args: T[]): void { + const argsLen = __TS__CountVarargs(...args); + const listLen = sparseArray.sparseLength; + for (const i of $range(1, argsLen)) { + sparseArray[listLen + i - 1] = args[i - 1]; + } + sparseArray.sparseLength = listLen + argsLen; +} diff --git a/src/lualib/Spread.ts b/src/lualib/Spread.ts new file mode 100644 index 000000000..3c736060d --- /dev/null +++ b/src/lualib/Spread.ts @@ -0,0 +1,15 @@ +export function __TS__Spread(this: void, iterable: string | Iterable): LuaMultiReturn { + const arr: T[] = []; + if (typeof iterable === "string") { + for (const i of $range(0, iterable.length - 1)) { + arr[i] = iterable[i] as T; + } + } else { + let len = 0; + for (const item of iterable) { + len++; + arr[len - 1] = item; + } + } + return $multi(...arr); +} diff --git a/src/lualib/StringAccess.ts b/src/lualib/StringAccess.ts new file mode 100644 index 000000000..c273506c2 --- /dev/null +++ b/src/lualib/StringAccess.ts @@ -0,0 +1,5 @@ +export function __TS__StringAccess(this: string, index: number) { + if (index >= 0 && index < this.length) { + return string.sub(this, index + 1, index + 1); + } +} diff --git a/src/lualib/StringCharAt.ts b/src/lualib/StringCharAt.ts new file mode 100644 index 000000000..d76e94654 --- /dev/null +++ b/src/lualib/StringCharAt.ts @@ -0,0 +1,5 @@ +export function __TS__StringCharAt(this: string, pos: number): string { + if (pos !== pos) pos = 0; + if (pos < 0) return ""; + return string.sub(this, pos + 1, pos + 1); +} diff --git a/src/lualib/StringCharCodeAt.ts b/src/lualib/StringCharCodeAt.ts new file mode 100644 index 000000000..a3c722b9d --- /dev/null +++ b/src/lualib/StringCharCodeAt.ts @@ -0,0 +1,5 @@ +export function __TS__StringCharCodeAt(this: string, index: number): number { + if (index !== index) index = 0; + if (index < 0) return NaN; + return string.byte(this, index + 1) ?? NaN; +} diff --git a/src/lualib/StringConcat.ts b/src/lualib/StringConcat.ts deleted file mode 100644 index ae90c6b61..000000000 --- a/src/lualib/StringConcat.ts +++ /dev/null @@ -1,7 +0,0 @@ -function __TS__StringConcat(str1: string, ...args: string[]): string { - let out = str1; - for (const arg of args) { - out = out + arg; - } - return out; -} diff --git a/src/lualib/StringEndsWith.ts b/src/lualib/StringEndsWith.ts new file mode 100644 index 000000000..ea7d9020e --- /dev/null +++ b/src/lualib/StringEndsWith.ts @@ -0,0 +1,7 @@ +export function __TS__StringEndsWith(this: string, searchString: string, endPosition?: number): boolean { + if (endPosition === undefined || endPosition > this.length) { + endPosition = this.length; + } + + return string.sub(this, endPosition - searchString.length + 1, endPosition) === searchString; +} diff --git a/src/lualib/StringIncludes.ts b/src/lualib/StringIncludes.ts new file mode 100644 index 000000000..3d896c8fa --- /dev/null +++ b/src/lualib/StringIncludes.ts @@ -0,0 +1,10 @@ +export function __TS__StringIncludes(this: string, searchString: string, position?: number): boolean { + // http://lua-users.org/wiki/StringLibraryTutorial + if (!position) { + position = 1; + } else { + position += 1; + } + const [index] = string.find(this, searchString, position, true); + return index !== undefined; +} diff --git a/src/lualib/StringPadEnd.ts b/src/lualib/StringPadEnd.ts new file mode 100644 index 000000000..1f1c30e2e --- /dev/null +++ b/src/lualib/StringPadEnd.ts @@ -0,0 +1,17 @@ +export function __TS__StringPadEnd(this: string, maxLength: number, fillString = " "): string { + if (maxLength !== maxLength) maxLength = 0; + if (maxLength === -Infinity || maxLength === Infinity) { + throw "Invalid string length"; + } + + if (this.length >= maxLength || fillString.length === 0) { + return this; + } + + maxLength -= this.length; + if (maxLength > fillString.length) { + fillString += fillString.repeat(maxLength / fillString.length); + } + + return this + string.sub(fillString, 1, Math.floor(maxLength)); +} diff --git a/src/lualib/StringPadStart.ts b/src/lualib/StringPadStart.ts new file mode 100644 index 000000000..a6cb8e601 --- /dev/null +++ b/src/lualib/StringPadStart.ts @@ -0,0 +1,17 @@ +export function __TS__StringPadStart(this: string, maxLength: number, fillString = " "): string { + if (maxLength !== maxLength) maxLength = 0; + if (maxLength === -Infinity || maxLength === Infinity) { + throw "Invalid string length"; + } + + if (this.length >= maxLength || fillString.length === 0) { + return this; + } + + maxLength -= this.length; + if (maxLength > fillString.length) { + fillString += fillString.repeat(maxLength / fillString.length); + } + + return string.sub(fillString, 1, Math.floor(maxLength)) + this; +} diff --git a/src/lualib/StringReplace.ts b/src/lualib/StringReplace.ts index 7324db680..9a2da017b 100644 --- a/src/lualib/StringReplace.ts +++ b/src/lualib/StringReplace.ts @@ -1,8 +1,17 @@ -declare namespace string { - /** @tupleReturn */ - function gsub(source: string, searchValue: string, replaceValue: string): [string, number]; -} - -function __TS__StringReplace(source: string, searchValue: string, replaceValue: string): string { - return string.gsub(source, searchValue, replaceValue)[0]; +const sub = string.sub; +export function __TS__StringReplace( + this: void, + source: string, + searchValue: string, + replaceValue: string | ((match: string, offset: number, string: string) => string) +): string { + const [startPos, endPos] = string.find(source, searchValue, undefined, true); + if (!startPos) { + return source; + } + const before = sub(source, 1, startPos - 1); + const replacement = + typeof replaceValue === "string" ? replaceValue : replaceValue(searchValue, startPos - 1, source); + const after = sub(source, endPos + 1); + return before + replacement + after; } diff --git a/src/lualib/StringReplaceAll.ts b/src/lualib/StringReplaceAll.ts new file mode 100644 index 000000000..c110512e9 --- /dev/null +++ b/src/lualib/StringReplaceAll.ts @@ -0,0 +1,41 @@ +const sub = string.sub; +const find = string.find; +export function __TS__StringReplaceAll( + this: void, + source: string, + searchValue: string, + replaceValue: string | ((match: string, offset: number, string: string) => string) +): string { + if (typeof replaceValue === "string") { + const concat = table.concat(source.split(searchValue), replaceValue); + if (searchValue.length === 0) { + return replaceValue + concat + replaceValue; + } + return concat; + } + const parts: string[] = []; + let partsIndex = 1; + + if (searchValue.length === 0) { + parts[0] = replaceValue("", 0, source); + partsIndex = 2; + for (const i of $range(1, source.length)) { + parts[partsIndex - 1] = sub(source, i, i); + parts[partsIndex] = replaceValue("", i, source); + partsIndex += 2; + } + } else { + let currentPos = 1; + while (true) { + const [startPos, endPos] = find(source, searchValue, currentPos, true); + if (!startPos) break; + parts[partsIndex - 1] = sub(source, currentPos, startPos - 1); + parts[partsIndex] = replaceValue(searchValue, startPos - 1, source); + partsIndex += 2; + + currentPos = endPos + 1; + } + parts[partsIndex - 1] = sub(source, currentPos); + } + return table.concat(parts); +} diff --git a/src/lualib/StringSlice.ts b/src/lualib/StringSlice.ts new file mode 100644 index 000000000..307d86894 --- /dev/null +++ b/src/lualib/StringSlice.ts @@ -0,0 +1,9 @@ +export function __TS__StringSlice(this: string, start?: number, end?: number): string { + if (start === undefined || start !== start) start = 0; + if (end !== end) end = 0; + + if (start >= 0) start += 1; + if (end !== undefined && end < 0) end -= 1; + + return string.sub(this, start, end); +} diff --git a/src/lualib/StringSplit.ts b/src/lualib/StringSplit.ts index 005f57436..f89ffb57d 100644 --- a/src/lualib/StringSplit.ts +++ b/src/lualib/StringSplit.ts @@ -1,36 +1,34 @@ -function __TS__StringSplit(source: string, separator?: string, limit?: number): string[] { - if (limit === undefined) { - limit = 4294967295; - } +const sub = string.sub; +const find = string.find; +export function __TS__StringSplit(this: void, source: string, separator?: string, limit?: number): string[] { + limit ??= 4294967295; if (limit === 0) { return []; } - const out = []; - let index = 0; - let count = 0; + const result = []; + let resultIndex = 1; if (separator === undefined || separator === "") { - while (index < source.length - 1 && count < limit) { - out[count] = source[index]; - count++; - index++; + for (const i of $range(1, source.length)) { + result[resultIndex - 1] = sub(source, i, i); + resultIndex++; } } else { - const separatorLength = separator.length; - let nextIndex = source.indexOf(separator); - while (nextIndex >= 0 && count < limit) { - out[count] = source.substring(index, nextIndex); - count++; - index = nextIndex + separatorLength; - nextIndex = source.indexOf(separator, index); + let currentPos = 1; + while (resultIndex <= limit) { + const [startPos, endPos] = find(source, separator, currentPos, true); + if (!startPos) break; + result[resultIndex - 1] = sub(source, currentPos, startPos - 1); + resultIndex++; + currentPos = endPos + 1; } - } - if (count < limit) { - out[count] = source.substring(index); + if (resultIndex <= limit) { + result[resultIndex - 1] = sub(source, currentPos); + } } - return out; + return result; } diff --git a/src/lualib/StringStartsWith.ts b/src/lualib/StringStartsWith.ts new file mode 100644 index 000000000..82f542143 --- /dev/null +++ b/src/lualib/StringStartsWith.ts @@ -0,0 +1,7 @@ +export function __TS__StringStartsWith(this: string, searchString: string, position?: number): boolean { + if (position === undefined || position < 0) { + position = 0; + } + + return string.sub(this, position + 1, searchString.length + position) === searchString; +} diff --git a/src/lualib/StringSubstr.ts b/src/lualib/StringSubstr.ts new file mode 100644 index 000000000..1940daf4d --- /dev/null +++ b/src/lualib/StringSubstr.ts @@ -0,0 +1,12 @@ +export function __TS__StringSubstr(this: string, from: number, length?: number): string { + if (from !== from) from = 0; + + if (length !== undefined) { + if (length !== length || length <= 0) return ""; + length += from; + } + + if (from >= 0) from += 1; + + return string.sub(this, from, length); +} diff --git a/src/lualib/StringSubstring.ts b/src/lualib/StringSubstring.ts new file mode 100644 index 000000000..f3480081a --- /dev/null +++ b/src/lualib/StringSubstring.ts @@ -0,0 +1,17 @@ +export function __TS__StringSubstring(this: string, start: number, end?: number): string { + if (end !== end) end = 0; + + if (end !== undefined && start > end) { + [start, end] = [end, start]; + } + + if (start >= 0) { + start += 1; + } else { + start = 1; + } + + if (end !== undefined && end < 0) end = 0; + + return string.sub(this, start, end); +} diff --git a/src/lualib/StringTrim.ts b/src/lualib/StringTrim.ts new file mode 100644 index 000000000..f1d71d712 --- /dev/null +++ b/src/lualib/StringTrim.ts @@ -0,0 +1,5 @@ +export function __TS__StringTrim(this: string): string { + // http://lua-users.org/wiki/StringTrim + const [result] = string.gsub(this, "^[%s\xA0\uFEFF]*(.-)[%s\xA0\uFEFF]*$", "%1"); + return result; +} diff --git a/src/lualib/StringTrimEnd.ts b/src/lualib/StringTrimEnd.ts new file mode 100644 index 000000000..711c91538 --- /dev/null +++ b/src/lualib/StringTrimEnd.ts @@ -0,0 +1,5 @@ +export function __TS__StringTrimEnd(this: string): string { + // http://lua-users.org/wiki/StringTrim + const [result] = string.gsub(this, "[%s\xA0\uFEFF]*$", ""); + return result; +} diff --git a/src/lualib/StringTrimStart.ts b/src/lualib/StringTrimStart.ts new file mode 100644 index 000000000..258ea4f8b --- /dev/null +++ b/src/lualib/StringTrimStart.ts @@ -0,0 +1,5 @@ +export function __TS__StringTrimStart(this: string): string { + // http://lua-users.org/wiki/StringTrim + const [result] = string.gsub(this, "^[%s\xA0\uFEFF]*", ""); + return result; +} diff --git a/src/lualib/Symbol.ts b/src/lualib/Symbol.ts index 2464b5f20..5c1d7076b 100644 --- a/src/lualib/Symbol.ts +++ b/src/lualib/Symbol.ts @@ -1,20 +1,20 @@ -declare function setmetatable(obj: T, metatable: any): T; - -// tslint:disable-next-line: variable-name -const ____symbolMetatable = { - __tostring(): string { - if (this.description === undefined) { - return 'Symbol()'; - } else { - return 'Symbol(' + this.description + ')'; - } +const symbolMetatable = { + __tostring(this: symbol): string { + return `Symbol(${this.description ?? ""})`; }, }; -function __TS__Symbol(description?: string | number): symbol { - return setmetatable({ description }, ____symbolMetatable) as any; +export function __TS__Symbol(this: void, description?: string | number): symbol { + return setmetatable({ description }, symbolMetatable) as unknown as symbol; } -Symbol = { - iterator: __TS__Symbol('Symbol.iterator'), +export const Symbol = { + asyncDispose: __TS__Symbol("Symbol.asyncDispose"), + dispose: __TS__Symbol("Symbol.dispose"), + iterator: __TS__Symbol("Symbol.iterator"), + hasInstance: __TS__Symbol("Symbol.hasInstance"), + + // Not implemented + species: __TS__Symbol("Symbol.species"), + toStringTag: __TS__Symbol("Symbol.toStringTag"), } as any; diff --git a/src/lualib/SymbolRegistry.ts b/src/lualib/SymbolRegistry.ts index c9c42e4e3..4deaa0993 100644 --- a/src/lualib/SymbolRegistry.ts +++ b/src/lualib/SymbolRegistry.ts @@ -1,16 +1,18 @@ -// tslint:disable-next-line: variable-name -const ____symbolRegistry: Record = {}; +import { __TS__Symbol } from "./Symbol"; -function __TS__SymbolRegistryFor(key: string): symbol { - if (!____symbolRegistry[key]) { - ____symbolRegistry[key] = __TS__Symbol(key); +const symbolRegistry: Record = {}; +export function __TS__SymbolRegistryFor(this: void, key: string): symbol { + if (!symbolRegistry[key]) { + symbolRegistry[key] = __TS__Symbol(key); } - return ____symbolRegistry[key]; + return symbolRegistry[key]; } -function __TS__SymbolRegistryKeyFor(sym: symbol): string { - for (const key in ____symbolRegistry) { - if (____symbolRegistry[key] === sym) return key; +export function __TS__SymbolRegistryKeyFor(this: void, sym: symbol): string | undefined { + for (const key in symbolRegistry) { + if (symbolRegistry[key] === sym) return key; } + + return undefined; } diff --git a/src/lualib/TypeOf.ts b/src/lualib/TypeOf.ts new file mode 100644 index 000000000..5afbbde04 --- /dev/null +++ b/src/lualib/TypeOf.ts @@ -0,0 +1,10 @@ +export function __TS__TypeOf(this: void, value: unknown): string { + const luaType = type(value); + if (luaType === "table") { + return "object"; + } else if (luaType === "nil") { + return "undefined"; + } else { + return luaType; + } +} diff --git a/src/lualib/Using.ts b/src/lualib/Using.ts new file mode 100644 index 000000000..a720f6e60 --- /dev/null +++ b/src/lualib/Using.ts @@ -0,0 +1,22 @@ +export function __TS__Using( + this: undefined, + cb: (this: void, ...args: TArgs) => TReturn, + ...args: TArgs +): TReturn { + let thrownError; + const [ok, result] = xpcall( + () => cb(...args), + err => (thrownError = err) + ); + + const argArray = [...args]; + for (let i = argArray.length - 1; i >= 0; i--) { + argArray[i][Symbol.dispose](); + } + + if (!ok) { + throw thrownError; + } + + return result; +} diff --git a/src/lualib/UsingAsync.ts b/src/lualib/UsingAsync.ts new file mode 100644 index 000000000..e4b7593f9 --- /dev/null +++ b/src/lualib/UsingAsync.ts @@ -0,0 +1,27 @@ +export async function __TS__UsingAsync, TReturn>( + this: undefined, + cb: (...args: TArgs) => TReturn, + ...args: TArgs +): Promise { + let thrownError; + const [ok, result] = xpcall( + () => cb(...args), + err => (thrownError = err) + ); + + const argArray = [...args]; + for (let i = argArray.length - 1; i >= 0; i--) { + if (Symbol.dispose in argArray[i]) { + (argArray[i] as Disposable)[Symbol.dispose](); + } + if (Symbol.asyncDispose in argArray[i]) { + await (argArray[i] as AsyncDisposable)[Symbol.asyncDispose](); + } + } + + if (!ok) { + throw thrownError; + } + + return result; +} diff --git a/src/lualib/WeakMap.ts b/src/lualib/WeakMap.ts index 3f92cf3df..65d1ae4da 100644 --- a/src/lualib/WeakMap.ts +++ b/src/lualib/WeakMap.ts @@ -1,49 +1,49 @@ -declare function setmetatable(obj: T, metatable: any): T; - -class WeakMap { - private items: {[key: string]: TValue}; // Type of key is actually TKey - - constructor(other: Iterable<[TKey, TValue]> | Array<[TKey, TValue]>) { - this.items = {}; - setmetatable(this.items, { __mode: 'k' }); - - if (other) { - const iterable = other as Iterable<[TKey, TValue]>; - if (iterable[Symbol.iterator]) { - // Iterate manually because WeakMap is compiled with ES5 which doesn't support Iterables in for...of - const iterator = iterable[Symbol.iterator](); - while (true) { - const result = iterator.next(); - if (result.done) { - break; - } - const value: [TKey, TValue] = result.value; // Ensures index is offset when tuple is accessed - this.set(value[0], value[1]); - } - } else { - for (const kvp of other as Array<[TKey, TValue]>) { - this.items[kvp[0] as any] = kvp[1]; +export class WeakMap { + public static [Symbol.species] = WeakMap; + public [Symbol.toStringTag] = "WeakMap"; + + private items = new LuaTable(); + + constructor(entries?: Iterable | Array) { + setmetatable(this.items, { __mode: "k" }); + if (entries === undefined) return; + + const iterable = entries as Iterable<[K, V]>; + if (iterable[Symbol.iterator]) { + // Iterate manually because WeakMap is compiled with ES5 which doesn't support Iterables in for...of + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + break; } + + const value: [K, V] = result.value; // Ensures index is offset when tuple is accessed + this.items.set(value[0], value[1]); + } + } else { + for (const kvp of entries as Array<[K, V]>) { + this.items.set(kvp[0], kvp[1]); } } } - public delete(key: TKey): boolean { + public delete(key: K): boolean { const contains = this.has(key); - this.items[key as any] = undefined; + this.items.set(key, undefined!); return contains; } - public get(key: TKey): TValue { - return this.items[key as any]; + public get(key: K): V | undefined { + return this.items.get(key); } - public has(key: TKey): boolean { - return this.items[key as any] !== undefined; + public has(key: K): boolean { + return this.items.get(key) !== undefined; } - public set(key: TKey, value: TValue): WeakMap { - this.items[key as any] = value; + public set(key: K, value: V): this { + this.items.set(key, value); return this; } } diff --git a/src/lualib/WeakSet.ts b/src/lualib/WeakSet.ts index 7cfae57f3..b921efc17 100644 --- a/src/lualib/WeakSet.ts +++ b/src/lualib/WeakSet.ts @@ -1,44 +1,43 @@ -declare function setmetatable(obj: T, metatable: any): T; +export class WeakSet { + public static [Symbol.species] = WeakSet; + public [Symbol.toStringTag] = "WeakSet"; -class WeakSet { - private items: {[key: string]: boolean}; // Key type is actually TValue + private items = new LuaTable(); - constructor(other: Iterable | TValue[]) { - this.items = {}; - setmetatable(this.items, { __mode: 'k' }); + constructor(values?: Iterable | T[]) { + setmetatable(this.items, { __mode: "k" }); + if (values === undefined) return; - if (other) { - const iterable = other as Iterable; - if (iterable[Symbol.iterator]) { - // Iterate manually because WeakSet is compiled with ES5 which doesn't support Iterables in for...of - const iterator = iterable[Symbol.iterator](); - while (true) { - const result = iterator.next(); - if (result.done) { - break; - } - this.add(result.value); - } - } else { - for (const value of other as TValue[]) { - this.items[value as any] = true; + const iterable = values as Iterable; + if (iterable[Symbol.iterator]) { + // Iterate manually because WeakSet is compiled with ES5 which doesn't support Iterables in for...of + const iterator = iterable[Symbol.iterator](); + while (true) { + const result = iterator.next(); + if (result.done) { + break; } + this.items.set(result.value, true); + } + } else { + for (const value of values as T[]) { + this.items.set(value, true); } } } - public add(value: TValue): WeakSet { - this.items[value as any] = true; + public add(value: T): this { + this.items.set(value, true); return this; } - public delete(value: TValue): boolean { + public delete(value: T): boolean { const contains = this.has(value); - this.items[value as any] = undefined; + this.items.set(value, undefined!); return contains; } - public has(value: TValue): boolean { - return this.items[value as any] === true; + public has(value: T): boolean { + return this.items.get(value) === true; } } diff --git a/src/lualib/declarations/tstl.d.ts b/src/lualib/declarations/tstl.d.ts new file mode 100644 index 000000000..9de44be80 --- /dev/null +++ b/src/lualib/declarations/tstl.d.ts @@ -0,0 +1,24 @@ +/** @noSelfInFile */ + +interface LuaMetatable { + _descriptors?: Record; + __index?: any; + __newindex?: any; + __tostring?: any; + __errorToStringPatched?: boolean; +} + +interface LuaClass extends LuaMetatable { + prototype: LuaClassInstance; + [Symbol.hasInstance]?(instance: LuaClassInstance): any; + ____super?: LuaClass; +} + +interface LuaClassInstance extends LuaMetatable { + constructor: LuaClass; +} + +// Declare math atan2 for versions that have it instead of math.atan +declare namespace math { + const atan2: typeof atan; +} diff --git a/src/lualib/declarations/universal/unpack.d.ts b/src/lualib/declarations/universal/unpack.d.ts new file mode 100644 index 000000000..c199c09df --- /dev/null +++ b/src/lualib/declarations/universal/unpack.d.ts @@ -0,0 +1,4 @@ +/** @noSelfInFile */ + +// Declare unpack for versions that have it instead of table.unpack +declare const unpack: typeof table.unpack | undefined; diff --git a/src/lualib/tsconfig.json b/src/lualib/tsconfig.json new file mode 100644 index 000000000..0f0874d0d --- /dev/null +++ b/src/lualib/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "../../dist/lualib/universal", + "target": "esnext", + "lib": ["esnext"], + "types": ["lua-types/5.4"], + "skipLibCheck": true, + "rootDirs": [".", "universal"], + + "strict": true + }, + "tstl": { + "luaLibImport": "none", + "noHeader": true, + "luaPlugins": [{ "name": "../../dist/lualib-build/plugin.js" }] + }, + "include": ["*.ts", "universal/**/*.ts", "declarations/**/*.ts", "../../language-extensions/index.d.ts"] +} diff --git a/src/lualib/tsconfig.lua50.json b/src/lualib/tsconfig.lua50.json new file mode 100644 index 000000000..a0b5aee4b --- /dev/null +++ b/src/lualib/tsconfig.lua50.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "outDir": "../../dist/lualib/5.0", + "target": "esnext", + "lib": ["esnext"], + "types": ["lua-types/5.0"], + "skipLibCheck": true, + "rootDirs": [".", "5.0"], + + "strict": true + }, + "tstl": { + "luaLibImport": "none", + "noHeader": true, + "luaTarget": "5.0", + "luaPlugins": [{ "name": "../../dist/lualib-build/plugin.js" }] + }, + "include": ["*.ts", "5.0/**/*.ts", "declarations/**/*.ts", "../../language-extensions/index.d.ts"] +} diff --git a/src/lualib/universal/CountVarargs.ts b/src/lualib/universal/CountVarargs.ts new file mode 100644 index 000000000..dc6107bb5 --- /dev/null +++ b/src/lualib/universal/CountVarargs.ts @@ -0,0 +1,6 @@ +/** @noSelfInFile */ + +export function __TS__CountVarargs(...args: T[]): number { + // Note that we need vararg optimization for this call. + return select("#", ...args); +} diff --git a/src/lualib/universal/Match.ts b/src/lualib/universal/Match.ts new file mode 100644 index 000000000..de9420b59 --- /dev/null +++ b/src/lualib/universal/Match.ts @@ -0,0 +1 @@ +export const __TS__Match = string.match; diff --git a/src/lualib/universal/MathModf.ts b/src/lualib/universal/MathModf.ts new file mode 100644 index 000000000..139ae8f6a --- /dev/null +++ b/src/lualib/universal/MathModf.ts @@ -0,0 +1 @@ +export const __TS__MathModf = math.modf; diff --git a/src/lualib/universal/SparseArraySpread.ts b/src/lualib/universal/SparseArraySpread.ts new file mode 100644 index 000000000..99871cb89 --- /dev/null +++ b/src/lualib/universal/SparseArraySpread.ts @@ -0,0 +1,6 @@ +import { __TS__SparseArray } from "./SparseArray"; + +export function __TS__SparseArraySpread(this: void, sparseArray: __TS__SparseArray): LuaMultiReturn { + const _unpack = unpack ?? table.unpack; + return _unpack(sparseArray, 1, sparseArray.sparseLength); +} diff --git a/src/lualib/universal/Unpack.ts b/src/lualib/universal/Unpack.ts new file mode 100644 index 000000000..f138f2d7d --- /dev/null +++ b/src/lualib/universal/Unpack.ts @@ -0,0 +1 @@ +export const __TS__Unpack = table.unpack || unpack; diff --git a/src/measure-performance.ts b/src/measure-performance.ts new file mode 100644 index 000000000..1d66af7e8 --- /dev/null +++ b/src/measure-performance.ts @@ -0,0 +1,83 @@ +import { performance } from "perf_hooks"; + +// We use our own performance hooks implementation for easier use, but also call node's performance hooks, so it shows up in the profiler. + +let enabled = false; +const marks = new Map(); +const durations = new Map(); + +function timestamp() { + return performance.now(); +} + +/** + * Marks a performance event, with the given markName. + */ +function mark(markName: string) { + if (enabled) { + marks.set(markName, timestamp()); + performance.mark(markName); + } +} + +/** + * Adds a performance measurement with the specified name. + * + * @param measureName The name of the performance measurement. + * @param startMarkName The name of the starting mark + * @param endMarkName The name of the ending mark + */ +function measure(measureName: string, startMarkName: string, endMarkName: string) { + if (enabled) { + const end = marks.get(endMarkName) ?? timestamp(); + const start = marks.get(startMarkName) ?? performance.timeOrigin; + const previousDuration = durations.get(measureName) ?? 0; + durations.set(measureName, previousDuration + (end - start)); + performance.measure(measureName, startMarkName, endMarkName); + } +} + +/** + * Starts a performance measurement section. + * @param name name of the measurement + */ +export function startSection(name: string) { + mark("start " + name); +} + +/** + * Ends a performance measurement section. + * @param name name of the measurement + */ +export function endSection(name: string) { + mark("end " + name); + measure(name, "start " + name, "end " + name); +} + +export function isMeasurementEnabled() { + return enabled; +} + +export function enableMeasurement() { + if (!enabled) { + enabled = true; + } +} + +export function disableMeasurement() { + if (enabled) { + enabled = false; + marks.clear(); + durations.clear(); + } +} + +export function forEachMeasure(callback: (measureName: string, duration: number) => void) { + durations.forEach((duration, measureName) => callback(measureName, duration)); +} + +export function getTotalDuration() { + let total = 0; + forEachMeasure((_, duration) => (total += duration)); + return total; +} diff --git a/src/transformation/builtins/array.ts b/src/transformation/builtins/array.ts new file mode 100644 index 000000000..b22bff01d --- /dev/null +++ b/src/transformation/builtins/array.ts @@ -0,0 +1,215 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformArguments, transformCallAndArguments } from "../visitors/call"; +import { expressionResultIsUsed, typeAlwaysHasSomeOfFlags } from "../utils/typescript"; +import { moveToPrecedingTemp } from "../visitors/expression-list"; +import { isUnpackCall, wrapInTable } from "../utils/lua-ast"; + +export function transformArrayConstructorCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const params = transformArguments(context, node.arguments, signature); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "from": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFrom, node, ...params); + case "isArray": + return transformLuaLibFunction(context, LuaLibFeature.ArrayIsArray, node, ...params); + case "of": + return wrapInTable(...params); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Array", expressionName)); + } +} + +function createTableLengthExpression(context: TransformationContext, expression: lua.Expression, node?: ts.Expression) { + if (context.luaTarget === LuaTarget.Lua50) { + const tableGetn = lua.createTableIndexExpression( + lua.createIdentifier("table"), + lua.createStringLiteral("getn") + ); + return lua.createCallExpression(tableGetn, [expression], node); + } else { + return lua.createUnaryExpression(expression, lua.SyntaxKind.LengthOperator, node); + } +} + +/** + * Optimized single element Array.push + * + * array[#array+1] = el + * return (#array + 1) + */ +function transformSingleElementArrayPush( + context: TransformationContext, + node: ts.CallExpression, + caller: lua.Expression, + param: lua.Expression +): lua.Expression { + const arrayIdentifier = lua.isIdentifier(caller) ? caller : moveToPrecedingTemp(context, caller); + + // #array + 1 + let lengthExpression: lua.Expression = lua.createBinaryExpression( + createTableLengthExpression(context, arrayIdentifier), + lua.createNumericLiteral(1), + lua.SyntaxKind.AdditionOperator + ); + + const expressionIsUsed = expressionResultIsUsed(node); + if (expressionIsUsed) { + // store length in a temp + lengthExpression = moveToPrecedingTemp(context, lengthExpression); + } + + const pushStatement = lua.createAssignmentStatement( + lua.createTableIndexExpression(arrayIdentifier, lengthExpression), + param, + node + ); + context.addPrecedingStatements(pushStatement); + return expressionIsUsed ? lengthExpression : lua.createNilLiteral(); +} + +export function transformArrayPrototypeCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const [caller, params] = transformCallAndArguments(context, calledMethod.expression, node.arguments, signature); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "at": + return transformLuaLibFunction(context, LuaLibFeature.ArrayAt, node, caller, ...params); + case "concat": + return transformLuaLibFunction(context, LuaLibFeature.ArrayConcat, node, caller, ...params); + case "entries": + return transformLuaLibFunction(context, LuaLibFeature.ArrayEntries, node, caller); + case "fill": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFill, node, caller, ...params); + case "push": + if (node.arguments.length === 1) { + const param = params[0] ?? lua.createNilLiteral(); + if (isUnpackCall(param)) { + return transformLuaLibFunction( + context, + LuaLibFeature.ArrayPushArray, + node, + caller, + (param as lua.CallExpression).params[0] ?? lua.createNilLiteral() + ); + } + if (!lua.isDotsLiteral(param)) { + return transformSingleElementArrayPush(context, node, caller, param); + } + } + + return transformLuaLibFunction(context, LuaLibFeature.ArrayPush, node, caller, ...params); + case "reverse": + return transformLuaLibFunction(context, LuaLibFeature.ArrayReverse, node, caller); + case "shift": + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("remove")), + [caller, lua.createNumericLiteral(1)], + node + ); + case "unshift": + return transformLuaLibFunction(context, LuaLibFeature.ArrayUnshift, node, caller, ...params); + case "sort": + return transformLuaLibFunction(context, LuaLibFeature.ArraySort, node, caller, ...params); + case "pop": + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("remove")), + [caller], + node + ); + case "forEach": + return transformLuaLibFunction(context, LuaLibFeature.ArrayForEach, node, caller, ...params); + case "find": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFind, node, caller, ...params); + case "findIndex": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFindIndex, node, caller, ...params); + case "includes": + return transformLuaLibFunction(context, LuaLibFeature.ArrayIncludes, node, caller, ...params); + case "indexOf": + return transformLuaLibFunction(context, LuaLibFeature.ArrayIndexOf, node, caller, ...params); + case "map": + return transformLuaLibFunction(context, LuaLibFeature.ArrayMap, node, caller, ...params); + case "filter": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFilter, node, caller, ...params); + case "reduce": + return transformLuaLibFunction(context, LuaLibFeature.ArrayReduce, node, caller, ...params); + case "reduceRight": + return transformLuaLibFunction(context, LuaLibFeature.ArrayReduceRight, node, caller, ...params); + case "some": + return transformLuaLibFunction(context, LuaLibFeature.ArraySome, node, caller, ...params); + case "every": + return transformLuaLibFunction(context, LuaLibFeature.ArrayEvery, node, caller, ...params); + case "slice": + return transformLuaLibFunction(context, LuaLibFeature.ArraySlice, node, caller, ...params); + case "splice": + return transformLuaLibFunction(context, LuaLibFeature.ArraySplice, node, caller, ...params); + case "join": + const callerType = context.checker.getTypeAtLocation(calledMethod.expression); + const elementType = context.checker.getElementTypeOfArrayType(callerType); + if ( + elementType && + typeAlwaysHasSomeOfFlags(context, elementType, ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike) + ) { + const defaultSeparatorLiteral = lua.createStringLiteral(","); + const param = params[0] ?? lua.createNilLiteral(); + const parameters = [ + caller, + node.arguments.length === 0 + ? defaultSeparatorLiteral + : lua.isStringLiteral(param) + ? param + : lua.createBinaryExpression(param, defaultSeparatorLiteral, lua.SyntaxKind.OrOperator), + ]; + + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("concat")), + parameters, + node + ); + } + + return transformLuaLibFunction(context, LuaLibFeature.ArrayJoin, node, caller, ...params); + case "flat": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFlat, node, caller, ...params); + case "flatMap": + return transformLuaLibFunction(context, LuaLibFeature.ArrayFlatMap, node, caller, ...params); + case "toReversed": + return transformLuaLibFunction(context, LuaLibFeature.ArrayToReversed, node, caller, ...params); + case "toSorted": + return transformLuaLibFunction(context, LuaLibFeature.ArrayToSorted, node, caller, ...params); + case "toSpliced": + return transformLuaLibFunction(context, LuaLibFeature.ArrayToSpliced, node, caller, ...params); + case "with": + return transformLuaLibFunction(context, LuaLibFeature.ArrayWith, node, caller, ...params); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "array", expressionName)); + } +} + +export function transformArrayProperty( + context: TransformationContext, + node: ts.PropertyAccessExpression +): lua.Expression | undefined { + switch (node.name.text) { + case "length": + const expression = context.transformExpression(node.expression); + return createTableLengthExpression(context, expression, node); + default: + return undefined; + } +} diff --git a/src/transformation/builtins/console.ts b/src/transformation/builtins/console.ts new file mode 100644 index 000000000..a32f547da --- /dev/null +++ b/src/transformation/builtins/console.ts @@ -0,0 +1,66 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { transformArguments } from "../visitors/call"; + +const isStringFormatTemplate = (node: ts.Expression) => ts.isStringLiteral(node) && node.text.includes("%"); + +export function transformConsoleCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const methodName = calledMethod.name.text; + const signature = context.checker.getResolvedSignature(node); + const parameters = transformArguments(context, node.arguments, signature); + + switch (methodName) { + case "error": + case "info": + case "log": + case "warn": + if (node.arguments.length > 0 && isStringFormatTemplate(node.arguments[0])) { + // print(string.format([arguments])) + const stringFormatCall = lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("string"), lua.createStringLiteral("format")), + parameters + ); + return lua.createCallExpression(lua.createIdentifier("print"), [stringFormatCall]); + } + // print([arguments]) + return lua.createCallExpression(lua.createIdentifier("print"), parameters); + case "assert": + if (node.arguments.length > 1 && isStringFormatTemplate(node.arguments[1])) { + // assert([condition], string.format([arguments])) + const stringFormatCall = lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("string"), lua.createStringLiteral("format")), + parameters.slice(1) + ); + return lua.createCallExpression(lua.createIdentifier("assert"), [parameters[0], stringFormatCall]); + } + // assert() + return lua.createCallExpression(lua.createIdentifier("assert"), parameters); + case "trace": + if (node.arguments.length > 0 && isStringFormatTemplate(node.arguments[0])) { + // print(debug.traceback(string.format([arguments]))) + const stringFormatCall = lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("string"), lua.createStringLiteral("format")), + parameters + ); + const debugTracebackCall = lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("debug"), lua.createStringLiteral("traceback")), + [stringFormatCall] + ); + return lua.createCallExpression(lua.createIdentifier("print"), [debugTracebackCall]); + } + // print(debug.traceback([arguments]))) + const debugTracebackCall = lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("debug"), lua.createStringLiteral("traceback")), + parameters + ); + return lua.createCallExpression(lua.createIdentifier("print"), [debugTracebackCall]); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "console", methodName)); + } +} diff --git a/src/transformation/builtins/function.ts b/src/transformation/builtins/function.ts new file mode 100644 index 000000000..8b219b6ba --- /dev/null +++ b/src/transformation/builtins/function.ts @@ -0,0 +1,70 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedForTarget, unsupportedProperty, unsupportedSelfFunctionConversion } from "../utils/diagnostics"; +import { ContextType, getFunctionContextType } from "../utils/function-context"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformCallAndArguments } from "../visitors/call"; +import { createUnpackCall } from "../utils/lua-ast"; + +export function transformFunctionPrototypeCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.CallExpression | undefined { + const callerType = context.checker.getTypeAtLocation(calledMethod.expression); + if (getFunctionContextType(context, callerType) === ContextType.Void) { + context.diagnostics.push(unsupportedSelfFunctionConversion(node)); + } + + const signature = context.checker.getResolvedSignature(node); + const [caller, params] = transformCallAndArguments(context, calledMethod.expression, node.arguments, signature); + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "apply": + const nonContextArgs = params.length > 1 ? [createUnpackCall(context, params[1], node.arguments[1])] : []; + return lua.createCallExpression(caller, [params[0], ...nonContextArgs], node); + case "bind": + return transformLuaLibFunction(context, LuaLibFeature.FunctionBind, node, caller, ...params); + case "call": + return lua.createCallExpression(caller, params, node); + case "toString": + context.diagnostics.push(unsupportedProperty(calledMethod.name, "function", expressionName)); + } +} + +export function transformFunctionProperty( + context: TransformationContext, + node: ts.PropertyAccessExpression +): lua.Expression | undefined { + switch (node.name.text) { + case "length": + if ( + context.luaTarget === LuaTarget.Lua50 || + context.luaTarget === LuaTarget.Lua51 || + context.luaTarget === LuaTarget.Universal + ) { + context.diagnostics.push(unsupportedForTarget(node, "function.length", context.luaTarget)); + } + + // debug.getinfo(fn) + const getInfoCall = lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("debug"), lua.createStringLiteral("getinfo")), + [context.transformExpression(node.expression)] + ); + + const nparams = lua.createTableIndexExpression(getInfoCall, lua.createStringLiteral("nparams")); + + const contextType = getFunctionContextType(context, context.checker.getTypeAtLocation(node.expression)); + return contextType === ContextType.NonVoid + ? lua.createBinaryExpression(nparams, lua.createNumericLiteral(1), lua.SyntaxKind.SubtractionOperator) + : nparams; + + case "arguments": + case "caller": + case "displayName": + case "name": + context.diagnostics.push(unsupportedProperty(node.name, "function", node.name.text)); + } +} diff --git a/src/transformation/builtins/global.ts b/src/transformation/builtins/global.ts new file mode 100644 index 000000000..288414a60 --- /dev/null +++ b/src/transformation/builtins/global.ts @@ -0,0 +1,44 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { isNumberType } from "../utils/typescript"; +import { transformArguments } from "../visitors/call"; +import { transformStringConstructorCall } from "./string"; + +export function tryTransformBuiltinGlobalCall( + context: TransformationContext, + node: ts.CallExpression, + expressionType: ts.Type +): lua.Expression | undefined { + function getParameters() { + const signature = context.checker.getResolvedSignature(node); + return transformArguments(context, node.arguments, signature); + } + + const name = expressionType.symbol.name; + switch (name) { + case "SymbolConstructor": + return transformLuaLibFunction(context, LuaLibFeature.Symbol, node, ...getParameters()); + case "NumberConstructor": + return transformLuaLibFunction(context, LuaLibFeature.Number, node, ...getParameters()); + case "StringConstructor": + return transformStringConstructorCall(node, ...getParameters()); + case "isNaN": + case "isFinite": + const numberParameters = isNumberType(context, expressionType) + ? getParameters() + : [transformLuaLibFunction(context, LuaLibFeature.Number, undefined, ...getParameters())]; + + return transformLuaLibFunction( + context, + name === "isNaN" ? LuaLibFeature.NumberIsNaN : LuaLibFeature.NumberIsFinite, + node, + ...numberParameters + ); + case "parseFloat": + return transformLuaLibFunction(context, LuaLibFeature.ParseFloat, node, ...getParameters()); + case "parseInt": + return transformLuaLibFunction(context, LuaLibFeature.ParseInt, node, ...getParameters()); + } +} diff --git a/src/transformation/builtins/index.ts b/src/transformation/builtins/index.ts new file mode 100644 index 000000000..3eefd64c9 --- /dev/null +++ b/src/transformation/builtins/index.ts @@ -0,0 +1,238 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { createNaN } from "../utils/lua-ast"; +import { importLuaLibFeature, LuaLibFeature } from "../utils/lualib"; +import { getIdentifierSymbolId } from "../utils/symbols"; +import { isStandardLibraryType, isStringType, isArrayType, isFunctionType } from "../utils/typescript"; +import { getCalledExpression } from "../visitors/call"; +import { transformArrayConstructorCall, transformArrayProperty, transformArrayPrototypeCall } from "./array"; +import { transformConsoleCall } from "./console"; +import { transformFunctionPrototypeCall, transformFunctionProperty } from "./function"; +import { tryTransformBuiltinGlobalCall } from "./global"; +import { transformMathCall, transformMathProperty } from "./math"; +import { transformNumberConstructorCall, transformNumberPrototypeCall, transformNumberProperty } from "./number"; +import { transformObjectConstructorCall, tryTransformObjectPrototypeCall } from "./object"; +import { transformPromiseConstructorCall } from "./promise"; +import { transformStringConstructorMethodCall, transformStringProperty, transformStringPrototypeCall } from "./string"; +import { transformSymbolConstructorCall } from "./symbol"; +import { unsupportedBuiltinOptionalCall } from "../utils/diagnostics"; +import { LuaTarget } from "../../CompilerOptions"; +import { transformMapConstructorCall } from "./map"; + +export function transformBuiltinPropertyAccessExpression( + context: TransformationContext, + node: ts.PropertyAccessExpression +): lua.Expression | undefined { + const ownerType = context.checker.getTypeAtLocation(node.expression); + + if (ts.isIdentifier(node.expression) && isStandardLibraryType(context, ownerType, undefined)) { + switch (ownerType.symbol.name) { + case "NumberConstructor": + return transformNumberProperty(context, node); + case "Math": + return transformMathProperty(context, node); + case "SymbolConstructor": + importLuaLibFeature(context, LuaLibFeature.Symbol); + } + } + + if (isStringType(context, ownerType)) { + return transformStringProperty(context, node); + } + + if (isArrayType(context, ownerType)) { + return transformArrayProperty(context, node); + } + + if (isFunctionType(ownerType)) { + return transformFunctionProperty(context, node); + } +} + +export function transformBuiltinCallExpression( + context: TransformationContext, + node: ts.CallExpression +): lua.Expression | undefined { + const expressionType = context.checker.getTypeAtLocation(node.expression); + if (ts.isIdentifier(node.expression) && isStandardLibraryType(context, expressionType, undefined)) { + checkForLuaLibType(context, expressionType); + const result = tryTransformBuiltinGlobalCall(context, node, expressionType); + if (result) return result; + } + + const calledMethod = ts.getOriginalNode(getCalledExpression(node)); + if (ts.isPropertyAccessExpression(calledMethod)) { + const globalResult = tryTransformBuiltinGlobalMethodCall(context, node, calledMethod); + if (globalResult) return globalResult; + + const prototypeResult = tryTransformBuiltinPropertyCall(context, node, calledMethod); + if (prototypeResult) return prototypeResult; + + // object prototype call may work even without resolved signature/type (which the other builtin calls use) + // e.g. (foo as any).toString() + // prototype methods take precedence (e.g. number.toString(2)) + const objectResult = tryTransformObjectPrototypeCall(context, node, calledMethod); + if (objectResult) return objectResult; + } +} + +function tryTransformBuiltinGlobalMethodCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +) { + const ownerType = context.checker.getTypeAtLocation(calledMethod.expression); + const ownerSymbol = tryGetStandardLibrarySymbolOfType(context, ownerType); + if (!ownerSymbol || ownerSymbol.parent) return; + + let result: lua.Expression | undefined; + switch (ownerSymbol.name) { + case "ArrayConstructor": + result = transformArrayConstructorCall(context, node, calledMethod); + break; + case "Console": + result = transformConsoleCall(context, node, calledMethod); + break; + case "MapConstructor": + result = transformMapConstructorCall(context, node, calledMethod); + break; + case "Math": + result = transformMathCall(context, node, calledMethod); + break; + case "StringConstructor": + result = transformStringConstructorMethodCall(context, node, calledMethod); + break; + case "ObjectConstructor": + result = transformObjectConstructorCall(context, node, calledMethod); + break; + case "SymbolConstructor": + result = transformSymbolConstructorCall(context, node, calledMethod); + break; + case "NumberConstructor": + result = transformNumberConstructorCall(context, node, calledMethod); + break; + case "PromiseConstructor": + result = transformPromiseConstructorCall(context, node, calledMethod); + break; + } + if (result && calledMethod.questionDotToken) { + // e.g. console?.log() + context.diagnostics.push(unsupportedBuiltinOptionalCall(calledMethod)); + } + return result; +} + +function tryTransformBuiltinPropertyCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +) { + const functionType = context.checker.getTypeAtLocation(node.expression); + const callSymbol = tryGetStandardLibrarySymbolOfType(context, functionType); + if (!callSymbol) return; + const ownerSymbol = callSymbol.parent; + if (!ownerSymbol || ownerSymbol.parent) return; + + switch (ownerSymbol.name) { + case "String": + return transformStringPrototypeCall(context, node, calledMethod); + case "Number": + return transformNumberPrototypeCall(context, node, calledMethod); + case "Array": + case "ReadonlyArray": + return transformArrayPrototypeCall(context, node, calledMethod); + case "Function": + case "CallableFunction": + case "NewableFunction": + return transformFunctionPrototypeCall(context, node, calledMethod); + } +} + +export function transformBuiltinIdentifierExpression( + context: TransformationContext, + node: ts.Identifier, + symbol: ts.Symbol | undefined +): lua.Expression | undefined { + switch (node.text) { + case "NaN": + return createNaN(node); + + case "Infinity": + if (context.luaTarget === LuaTarget.Lua50) { + const one = lua.createNumericLiteral(1); + const zero = lua.createNumericLiteral(0); + return lua.createBinaryExpression(one, zero, lua.SyntaxKind.DivisionOperator); + } else { + const math = lua.createIdentifier("math"); + const huge = lua.createStringLiteral("huge"); + return lua.createTableIndexExpression(math, huge, node); + } + case "globalThis": + return lua.createIdentifier("_G", node, getIdentifierSymbolId(context, node, symbol), "globalThis"); + } +} + +const builtinErrorTypeNames = new Set([ + "Error", + "ErrorConstructor", + "RangeError", + "RangeErrorConstructor", + "ReferenceError", + "ReferenceErrorConstructor", + "SyntaxError", + "SyntaxErrorConstructor", + "TypeError", + "TypeErrorConstructor", + "URIError", + "URIErrorConstructor", +]); + +export function checkForLuaLibType(context: TransformationContext, type: ts.Type): void { + const symbol = type.symbol; + if (!symbol || symbol.parent) return; + const name = symbol.name; + + switch (name) { + case "Map": + case "MapConstructor": + importLuaLibFeature(context, LuaLibFeature.Map); + return; + case "Set": + case "SetConstructor": + importLuaLibFeature(context, LuaLibFeature.Set); + return; + case "WeakMap": + case "WeakMapConstructor": + importLuaLibFeature(context, LuaLibFeature.WeakMap); + return; + case "WeakSet": + case "WeakSetConstructor": + importLuaLibFeature(context, LuaLibFeature.WeakSet); + return; + case "Promise": + case "PromiseConstructor": + importLuaLibFeature(context, LuaLibFeature.Promise); + return; + } + + if (builtinErrorTypeNames.has(name)) { + importLuaLibFeature(context, LuaLibFeature.Error); + } +} + +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); + if (symbol) return symbol; + } + } else if (isStandardLibraryType(context, type, undefined)) { + return type.symbol; + } + + return undefined; +} diff --git a/src/transformation/builtins/map.ts b/src/transformation/builtins/map.ts new file mode 100644 index 000000000..556578591 --- /dev/null +++ b/src/transformation/builtins/map.ts @@ -0,0 +1,22 @@ +import * as lua from "../../LuaAST"; +import * as ts from "typescript"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { transformArguments } from "../visitors/call"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; + +export function transformMapConstructorCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const args = transformArguments(context, node.arguments); + const methodName = calledMethod.name.text; + + switch (methodName) { + case "groupBy": + return transformLuaLibFunction(context, LuaLibFeature.MapGroupBy, node, ...args); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Map", methodName)); + } +} diff --git a/src/transformation/builtins/math.ts b/src/transformation/builtins/math.ts new file mode 100644 index 000000000..ab831d52f --- /dev/null +++ b/src/transformation/builtins/math.ts @@ -0,0 +1,129 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformArguments } from "../visitors/call"; + +export function transformMathProperty( + context: TransformationContext, + node: ts.PropertyAccessExpression +): lua.Expression | undefined { + const name = node.name.text; + switch (name) { + case "PI": + const property = lua.createStringLiteral("pi", node.name); + const math = lua.createIdentifier("math", node.expression); + return lua.createTableIndexExpression(math, property, node); + + case "E": + case "LN10": + case "LN2": + case "LOG10E": + case "LOG2E": + case "SQRT1_2": + case "SQRT2": + return lua.createNumericLiteral(Math[name], node); + + default: + context.diagnostics.push(unsupportedProperty(node.name, "Math", name)); + } +} + +export function transformMathCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const params = transformArguments(context, node.arguments, signature); + const math = lua.createIdentifier("math"); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + // 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"); + return lua.createCallExpression(lua.createTableIndexExpression(math, method), params, node); + } + + // (math.log(x) / Math.LNe) + case "log10": + case "log2": { + const log1 = lua.createTableIndexExpression(math, lua.createStringLiteral("log")); + const logCall1 = lua.createCallExpression(log1, params); + const e = lua.createNumericLiteral(expressionName === "log10" ? Math.LN10 : Math.LN2); + return lua.createBinaryExpression(logCall1, e, lua.SyntaxKind.DivisionOperator, node); + } + + // math.log(1 + x) + case "log1p": { + const log = lua.createStringLiteral("log"); + const one = lua.createNumericLiteral(1); + const add = lua.createBinaryExpression( + one, + params[0] ?? lua.createNilLiteral(), + lua.SyntaxKind.AdditionOperator + ); + return lua.createCallExpression(lua.createTableIndexExpression(math, log), [add], node); + } + + case "pow": { + // Translate to base ^ power + return lua.createBinaryExpression( + params[0] ?? lua.createNilLiteral(), + params[1] ?? lua.createNilLiteral(), + lua.SyntaxKind.PowerOperator, + node + ); + } + + // math.floor(x + 0.5) + case "round": { + const floor = lua.createStringLiteral("floor"); + const half = lua.createNumericLiteral(0.5); + const add = lua.createBinaryExpression( + params[0] ?? lua.createNilLiteral(), + half, + lua.SyntaxKind.AdditionOperator + ); + return lua.createCallExpression(lua.createTableIndexExpression(math, floor), [add], node); + } + + case "sign": { + return transformLuaLibFunction(context, LuaLibFeature.MathSign, node, ...params); + } + + case "trunc": { + return transformLuaLibFunction(context, LuaLibFeature.MathTrunc, node, ...params); + } + + case "abs": + case "acos": + case "asin": + case "atan": + case "ceil": + case "cos": + case "exp": + case "floor": + case "log": + case "max": + case "min": + case "random": + case "sin": + case "sqrt": + case "tan": { + const method = lua.createStringLiteral(expressionName); + return lua.createCallExpression(lua.createTableIndexExpression(math, method), params, node); + } + + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Math", expressionName)); + } +} diff --git a/src/transformation/builtins/number.ts b/src/transformation/builtins/number.ts new file mode 100644 index 000000000..17c44b6fd --- /dev/null +++ b/src/transformation/builtins/number.ts @@ -0,0 +1,134 @@ +import ts = require("typescript"); +import * as lua from "../../LuaAST"; +import { createNaN } from "../utils/lua-ast"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformArguments } from "../visitors/call"; +import { LuaTarget } from "../../CompilerOptions"; + +export function transformNumberPrototypeCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const params = transformArguments(context, node.arguments, signature); + const caller = context.transformExpression(calledMethod.expression); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "toString": + return params.length === 0 + ? lua.createCallExpression(lua.createIdentifier("tostring"), [caller], node) + : transformLuaLibFunction(context, LuaLibFeature.NumberToString, node, caller, ...params); + case "toFixed": + return transformLuaLibFunction(context, LuaLibFeature.NumberToFixed, node, caller, ...params); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "number", expressionName)); + } +} + +export function transformNumberProperty( + context: TransformationContext, + node: ts.PropertyAccessExpression +): lua.Expression | undefined { + const name = node.name.text; + + /* + Read the docs on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number for further info about what these numbers entail. + Most of them should be fairly straight forward base on their name(s) though. + */ + + switch (name) { + case "POSITIVE_INFINITY": + if (context.luaTarget === LuaTarget.Lua50) { + const one = lua.createNumericLiteral(1); + const zero = lua.createNumericLiteral(0); + return lua.createBinaryExpression(one, zero, lua.SyntaxKind.DivisionOperator); + } else { + const math = lua.createIdentifier("math"); + const huge = lua.createStringLiteral("huge"); + return lua.createTableIndexExpression(math, huge, node); + } + case "NEGATIVE_INFINITY": + if (context.luaTarget === LuaTarget.Lua50) { + const one = lua.createNumericLiteral(1); + const zero = lua.createNumericLiteral(0); + return lua.createUnaryExpression( + lua.createBinaryExpression(one, zero, lua.SyntaxKind.DivisionOperator), + lua.SyntaxKind.NegationOperator + ); + } else { + const math = lua.createIdentifier("math"); + const huge = lua.createStringLiteral("huge"); + return lua.createUnaryExpression( + lua.createTableIndexExpression(math, huge, node), + lua.SyntaxKind.NegationOperator + ); + } + case "NaN": + return createNaN(node); + case "EPSILON": + return lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(-52), + lua.SyntaxKind.PowerOperator, + node + ); + case "MIN_VALUE": + return lua.createBinaryExpression( + 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, + node + ); + case "MAX_SAFE_INTEGER": + return lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(1024), + lua.SyntaxKind.PowerOperator, + node + ); + case "MAX_VALUE": + return lua.createBinaryExpression( + lua.createNumericLiteral(2), + lua.createNumericLiteral(1024), + lua.SyntaxKind.PowerOperator, + node + ); + + default: + context.diagnostics.push(unsupportedProperty(node.name, "Number", name)); + } +} + +export function transformNumberConstructorCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.CallExpression | undefined { + const parameters = transformArguments(context, node.arguments); + const methodName = calledMethod.name.text; + switch (methodName) { + case "isInteger": + return transformLuaLibFunction(context, LuaLibFeature.NumberIsInteger, node, ...parameters); + case "isNaN": + return transformLuaLibFunction(context, LuaLibFeature.NumberIsNaN, node, ...parameters); + case "isFinite": + return transformLuaLibFunction(context, LuaLibFeature.NumberIsFinite, node, ...parameters); + case "parseInt": + return transformLuaLibFunction(context, LuaLibFeature.NumberParseInt, node, ...parameters); + case "parseFloat": + return transformLuaLibFunction(context, LuaLibFeature.NumberParseFloat, node, ...parameters); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Number", methodName)); + } +} diff --git a/src/transformation/builtins/object.ts b/src/transformation/builtins/object.ts new file mode 100644 index 000000000..43dd8a165 --- /dev/null +++ b/src/transformation/builtins/object.ts @@ -0,0 +1,67 @@ +import * as lua from "../../LuaAST"; +import * as ts from "typescript"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformArguments } from "../visitors/call"; + +export function transformObjectConstructorCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const args = transformArguments(context, node.arguments); + const methodName = calledMethod.name.text; + + switch (methodName) { + case "assign": + return transformLuaLibFunction(context, LuaLibFeature.ObjectAssign, node, ...args); + case "defineProperty": + return transformLuaLibFunction(context, LuaLibFeature.ObjectDefineProperty, node, ...args); + case "entries": + return transformLuaLibFunction(context, LuaLibFeature.ObjectEntries, node, ...args); + case "fromEntries": + return transformLuaLibFunction(context, LuaLibFeature.ObjectFromEntries, node, ...args); + case "getOwnPropertyDescriptor": + return transformLuaLibFunction(context, LuaLibFeature.ObjectGetOwnPropertyDescriptor, node, ...args); + case "getOwnPropertyDescriptors": + return transformLuaLibFunction(context, LuaLibFeature.ObjectGetOwnPropertyDescriptors, node, ...args); + case "groupBy": + return transformLuaLibFunction(context, LuaLibFeature.ObjectGroupBy, node, ...args); + case "keys": + return transformLuaLibFunction(context, LuaLibFeature.ObjectKeys, node, ...args); + case "values": + return transformLuaLibFunction(context, LuaLibFeature.ObjectValues, node, ...args); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Object", methodName)); + } +} + +export function tryTransformObjectPrototypeCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const name = calledMethod.name.text; + switch (name) { + case "toString": + const toStringIdentifier = lua.createIdentifier("tostring"); + return lua.createCallExpression( + toStringIdentifier, + [context.transformExpression(calledMethod.expression)], + node + ); + case "hasOwnProperty": + const expr = context.transformExpression(calledMethod.expression); + const signature = context.checker.getResolvedSignature(node); + const parameters = transformArguments(context, node.arguments, signature); + const rawGetIdentifier = lua.createIdentifier("rawget"); + const rawGetCall = lua.createCallExpression(rawGetIdentifier, [expr, ...parameters]); + return lua.createBinaryExpression( + rawGetCall, + lua.createNilLiteral(), + lua.SyntaxKind.InequalityOperator, + node + ); + } +} diff --git a/src/transformation/builtins/promise.ts b/src/transformation/builtins/promise.ts new file mode 100644 index 000000000..19c7284e7 --- /dev/null +++ b/src/transformation/builtins/promise.ts @@ -0,0 +1,54 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { importLuaLibFeature, LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformArguments } from "../visitors/call"; +import { isStandardLibraryType } from "../utils/typescript"; + +export function isPromiseClass(context: TransformationContext, node: ts.Identifier) { + if (node.text !== "Promise") return false; + const type = context.checker.getTypeAtLocation(node); + return isStandardLibraryType(context, type, undefined); +} + +export function createPromiseIdentifier(original: ts.Node) { + return lua.createIdentifier("__TS__Promise", original); +} + +export function transformPromiseConstructorCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const params = transformArguments(context, node.arguments, signature); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "all": + return transformLuaLibFunction(context, LuaLibFeature.PromiseAll, node, ...params); + case "allSettled": + return transformLuaLibFunction(context, LuaLibFeature.PromiseAllSettled, node, ...params); + case "any": + return transformLuaLibFunction(context, LuaLibFeature.PromiseAny, node, ...params); + case "race": + return transformLuaLibFunction(context, LuaLibFeature.PromiseRace, node, ...params); + case "resolve": + importLuaLibFeature(context, LuaLibFeature.Promise); + return lua.createCallExpression(createStaticPromiseFunctionAccessor("resolve", calledMethod), params, node); + case "reject": + importLuaLibFeature(context, LuaLibFeature.Promise); + return lua.createCallExpression(createStaticPromiseFunctionAccessor("reject", calledMethod), params, node); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Promise", expressionName)); + } +} + +export function createStaticPromiseFunctionAccessor(functionName: string, node: ts.Node) { + return lua.createTableIndexExpression( + lua.createIdentifier("__TS__Promise"), + lua.createStringLiteral(functionName), + node + ); +} diff --git a/src/transformation/builtins/string.ts b/src/transformation/builtins/string.ts new file mode 100644 index 000000000..98bd62303 --- /dev/null +++ b/src/transformation/builtins/string.ts @@ -0,0 +1,212 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { addToNumericExpression, createNaN, getNumberLiteralValue, wrapInTable } from "../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformArguments, transformCallAndArguments } from "../visitors/call"; + +function createStringCall(methodName: string, tsOriginal: ts.Node, ...params: lua.Expression[]): lua.CallExpression { + const stringIdentifier = lua.createIdentifier("string"); + return lua.createCallExpression( + lua.createTableIndexExpression(stringIdentifier, lua.createStringLiteral(methodName)), + params, + tsOriginal + ); +} + +export function transformStringPrototypeCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const [caller, params] = transformCallAndArguments(context, calledMethod.expression, node.arguments, signature); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "replace": + return transformLuaLibFunction(context, LuaLibFeature.StringReplace, node, caller, ...params); + case "replaceAll": + return transformLuaLibFunction(context, LuaLibFeature.StringReplaceAll, node, caller, ...params); + case "concat": + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("table"), lua.createStringLiteral("concat")), + [wrapInTable(caller, ...params)], + node + ); + + case "indexOf": { + const stringExpression = createStringCall( + "find", + node, + caller, + params[0] ?? lua.createNilLiteral(), + params[1] + ? // string.find handles negative indexes by making it relative to string end, but for indexOf it's the same as 0 + lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("math"), lua.createStringLiteral("max")), + [addToNumericExpression(params[1], 1), lua.createNumericLiteral(1)] + ) + : lua.createNilLiteral(), + lua.createBooleanLiteral(true) + ); + + return lua.createBinaryExpression( + lua.createBinaryExpression(stringExpression, lua.createNumericLiteral(0), lua.SyntaxKind.OrOperator), + lua.createNumericLiteral(1), + lua.SyntaxKind.SubtractionOperator, + node + ); + } + + case "substr": + return transformLuaLibFunction(context, LuaLibFeature.StringSubstr, node, caller, ...params); + case "substring": + return transformLuaLibFunction(context, LuaLibFeature.StringSubstring, node, caller, ...params); + + case "slice": { + const literalArg1 = getNumberLiteralValue(params[0]); + if (params[0] && literalArg1 !== undefined) { + let stringSubArgs: lua.Expression[] | undefined = [ + addToNumericExpression(params[0], literalArg1 < 0 ? 0 : 1), + ]; + + if (params[1]) { + const literalArg2 = getNumberLiteralValue(params[1]); + if (literalArg2 !== undefined) { + stringSubArgs.push(addToNumericExpression(params[1], literalArg2 < 0 ? -1 : 0)); + } else { + stringSubArgs = undefined; + } + } + + // Inline string.sub call if we know that both parameters are pure and aren't negative + if (stringSubArgs) { + return createStringCall("sub", node, caller, ...stringSubArgs); + } + } + + return transformLuaLibFunction(context, LuaLibFeature.StringSlice, node, caller, ...params); + } + + case "toLowerCase": + return createStringCall("lower", node, caller); + case "toUpperCase": + return createStringCall("upper", node, caller); + case "trim": + return transformLuaLibFunction(context, LuaLibFeature.StringTrim, node, caller); + case "trimEnd": + case "trimRight": + return transformLuaLibFunction(context, LuaLibFeature.StringTrimEnd, node, caller); + case "trimStart": + case "trimLeft": + return transformLuaLibFunction(context, LuaLibFeature.StringTrimStart, node, caller); + case "split": + return transformLuaLibFunction(context, LuaLibFeature.StringSplit, node, caller, ...params); + + case "charAt": { + const literalValue = getNumberLiteralValue(params[0]); + // Inline string.sub call if we know that parameter is pure and isn't negative + if (literalValue !== undefined && literalValue >= 0) { + const firstParamPlusOne = addToNumericExpression(params[0] ?? lua.createNilLiteral(), 1); + return createStringCall("sub", node, caller, firstParamPlusOne, firstParamPlusOne); + } + + return transformLuaLibFunction(context, LuaLibFeature.StringCharAt, node, caller, ...params); + } + + case "charCodeAt": { + const literalValue = getNumberLiteralValue(params[0]); + // Inline string.sub call if we know that parameter is pure and isn't negative + if (literalValue !== undefined && literalValue >= 0) { + return lua.createBinaryExpression( + createStringCall( + "byte", + node, + caller, + addToNumericExpression(params[0] ?? lua.createNilLiteral(), 1) + ), + createNaN(), + lua.SyntaxKind.OrOperator + ); + } + + return transformLuaLibFunction(context, LuaLibFeature.StringCharCodeAt, node, caller, ...params); + } + + case "startsWith": + return transformLuaLibFunction(context, LuaLibFeature.StringStartsWith, node, caller, ...params); + case "endsWith": + return transformLuaLibFunction(context, LuaLibFeature.StringEndsWith, node, caller, ...params); + case "includes": + return transformLuaLibFunction(context, LuaLibFeature.StringIncludes, node, caller, ...params); + case "repeat": + const math = lua.createIdentifier("math"); + const floor = lua.createStringLiteral("floor"); + const parameter = lua.createCallExpression(lua.createTableIndexExpression(math, floor), [ + params[0] ?? lua.createNilLiteral(), + ]); + return createStringCall("rep", node, caller, parameter); + case "padStart": + return transformLuaLibFunction(context, LuaLibFeature.StringPadStart, node, caller, ...params); + case "padEnd": + return transformLuaLibFunction(context, LuaLibFeature.StringPadEnd, node, caller, ...params); + case "toString": + return; // will be handled by transformObjectPrototypeCall + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "string", expressionName)); + } +} + +export function transformStringConstructorMethodCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression | undefined { + const signature = context.checker.getResolvedSignature(node); + const params = transformArguments(context, node.arguments, signature); + + const expressionName = calledMethod.name.text; + switch (expressionName) { + case "fromCharCode": + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("string"), lua.createStringLiteral("char")), + params, + node + ); + + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "String", expressionName)); + } +} + +export function transformStringProperty( + context: TransformationContext, + node: ts.PropertyAccessExpression +): lua.Expression | undefined { + switch (node.name.text) { + case "length": + const expression = context.transformExpression(node.expression); + if (context.luaTarget === LuaTarget.Lua50) { + const stringLen = lua.createTableIndexExpression( + lua.createIdentifier("string"), + lua.createStringLiteral("len") + ); + return lua.createCallExpression(stringLen, [expression], node); + } else { + return lua.createUnaryExpression(expression, lua.SyntaxKind.LengthOperator, node); + } + default: + context.diagnostics.push(unsupportedProperty(node.name, "string", node.name.text)); + } +} + +export function transformStringConstructorCall( + originalNode: ts.CallExpression, + ...args: lua.Expression[] +): lua.Expression | undefined { + const tostring = lua.createIdentifier("tostring", originalNode.expression); + return lua.createCallExpression(tostring, args, originalNode); +} diff --git a/src/transformation/builtins/symbol.ts b/src/transformation/builtins/symbol.ts new file mode 100644 index 000000000..4c5b7cbde --- /dev/null +++ b/src/transformation/builtins/symbol.ts @@ -0,0 +1,26 @@ +import ts = require("typescript"); +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { importLuaLibFeature, LuaLibFeature } from "../utils/lualib"; +import { transformArguments } from "../visitors/call"; + +export function transformSymbolConstructorCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.CallExpression | undefined { + const signature = context.checker.getResolvedSignature(node); + const parameters = transformArguments(context, node.arguments, signature); + const methodName = calledMethod.name.text; + switch (methodName) { + case "for": + case "keyFor": + importLuaLibFeature(context, LuaLibFeature.SymbolRegistry); + const upperMethodName = methodName[0].toUpperCase() + methodName.slice(1); + const functionIdentifier = lua.createIdentifier(`__TS__SymbolRegistry${upperMethodName}`); + return lua.createCallExpression(functionIdentifier, parameters, node); + default: + context.diagnostics.push(unsupportedProperty(calledMethod.name, "Symbol", methodName)); + } +} diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts new file mode 100644 index 000000000..ad40d48c5 --- /dev/null +++ b/src/transformation/context/context.ts @@ -0,0 +1,269 @@ +import * as ts from "typescript"; +import { CompilerOptions, LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { assert, castArray } from "../../utils"; +import { unsupportedNodeKind } from "../utils/diagnostics"; +import { unwrapVisitorResult, OneToManyVisitorResult } from "../utils/lua-ast"; +import { createSafeName } from "../utils/safe-names"; +import { ExpressionLikeNode, StatementLikeNode, VisitorMap, FunctionVisitor } from "./visitors"; +import { SymbolInfo } from "../utils/symbols"; +import { LuaLibFeature } from "../../LuaLib"; +import { Scope, ScopeType } from "../utils/scope"; +import { ClassSuperInfo } from "../visitors/class"; + +export const tempSymbolId = -1 as lua.SymbolId; + +export interface AllAccessorDeclarations { + firstAccessor: ts.AccessorDeclaration; + getAccessor: ts.GetAccessorDeclaration | undefined; + setAccessor: ts.SetAccessorDeclaration | undefined; +} + +export interface EmitResolver { + isValueAliasDeclaration(node: ts.Node): boolean; + isReferencedAliasDeclaration(node: ts.Node, checkChildren?: boolean): boolean; + isTopLevelValueImportEqualsWithEntityName(node: ts.ImportEqualsDeclaration): boolean; +} + +export interface TypeCheckerWithEmitResolver extends ts.TypeChecker { + getEmitResolver(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): EmitResolver; +} + +export class TransformationContext { + public readonly diagnostics: ts.Diagnostic[] = []; + public readonly checker = this.program.getTypeChecker() as TypeCheckerWithEmitResolver; + public readonly resolver: EmitResolver; + public readonly precedingStatementsStack: lua.Statement[][] = []; + + public readonly options: CompilerOptions = this.program.getCompilerOptions(); + public readonly luaTarget = this.options.luaTarget ?? LuaTarget.Universal; + public readonly isModule = ts.isExternalModule(this.sourceFile); + public readonly isStrict = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (this.options.alwaysStrict ?? this.options.strict) || + (this.isModule && this.options.target !== undefined && this.options.target >= ts.ScriptTarget.ES2015); + + constructor(public program: ts.Program, public sourceFile: ts.SourceFile, private visitorMap: VisitorMap) { + // Use `getParseTreeNode` to get original SourceFile node, before it was substituted by custom transformers. + // It's required because otherwise `getEmitResolver` won't use cached diagnostics, produced in `emitWorker` + // and would try to re-analyze the file, which would fail because of replaced nodes. + const originalSourceFile = ts.getParseTreeNode(sourceFile, ts.isSourceFile) ?? sourceFile; + this.resolver = this.checker.getEmitResolver(originalSourceFile); + } + + private currentNodeVisitors: ReadonlyArray> = []; + private currentNodeVisitorsIndex = 0; + + private nextTempId = 0; + + public transformNode(node: ts.Node): lua.Node[] { + return unwrapVisitorResult(this.transformNodeRaw(node)); + } + + /** @internal */ + public transformNodeRaw(node: ts.Node, isExpression?: boolean) { + // TODO: Move to visitors? + if ( + ts.canHaveModifiers(node) && + node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword) + ) { + return []; + } + + const nodeVisitors = this.visitorMap.get(node.kind); + if (!nodeVisitors) { + this.diagnostics.push(unsupportedNodeKind(node, node.kind)); + return isExpression ? [lua.createNilLiteral()] : []; + } + + const previousNodeVisitors = this.currentNodeVisitors; + const previousNodeVisitorsIndex = this.currentNodeVisitorsIndex; + this.currentNodeVisitors = nodeVisitors; + this.currentNodeVisitorsIndex = nodeVisitors.length - 1; + + const visitor = this.currentNodeVisitors[this.currentNodeVisitorsIndex]; + const result = visitor(node, this); + + this.currentNodeVisitors = previousNodeVisitors; + this.currentNodeVisitorsIndex = previousNodeVisitorsIndex; + + return result; + } + + public superTransformNode(node: ts.Node): lua.Node[] { + return unwrapVisitorResult(this.doSuperTransformNode(node)); + } + + private doSuperTransformNode(node: ts.Node): OneToManyVisitorResult { + if (--this.currentNodeVisitorsIndex < 0) { + throw new Error(`There is no super transform for ${ts.SyntaxKind[node.kind]} visitor`); + } + + const visitor = this.currentNodeVisitors[this.currentNodeVisitorsIndex]; + return unwrapVisitorResult(visitor(node, this)); + } + + public transformExpression(node: ExpressionLikeNode): lua.Expression { + const result = this.transformNodeRaw(node, true); + return this.assertIsExpression(node, result); + } + + private assertIsExpression(node: ExpressionLikeNode, result: OneToManyVisitorResult): lua.Expression { + if (result === undefined) { + throw new Error(`Expression visitor for node type ${ts.SyntaxKind[node.kind]} did not return any result.`); + } + if (Array.isArray(result)) { + return result[0] as lua.Expression; + } + return result as lua.Expression; + } + + public superTransformExpression(node: ExpressionLikeNode): lua.Expression { + const result = this.doSuperTransformNode(node); + return this.assertIsExpression(node, result); + } + + public transformStatements(node: StatementLikeNode | readonly StatementLikeNode[]): lua.Statement[] { + return castArray(node).flatMap(n => { + this.pushPrecedingStatements(); + const statements = this.transformNode(n) as lua.Statement[]; + const result = this.popPrecedingStatements(); + result.push(...statements); + return result; + }); + } + + public superTransformStatements(node: StatementLikeNode | readonly StatementLikeNode[]): lua.Statement[] { + return castArray(node).flatMap(n => { + this.pushPrecedingStatements(); + const statements = this.superTransformNode(n) as lua.Statement[]; + const result = this.popPrecedingStatements(); + result.push(...statements); + return result; + }); + } + + public pushPrecedingStatements() { + this.precedingStatementsStack.push([]); + } + + public popPrecedingStatements() { + const precedingStatements = this.precedingStatementsStack.pop(); + assert(precedingStatements); + return precedingStatements; + } + + public addPrecedingStatements(statements: lua.Statement | lua.Statement[]) { + const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; + assert(precedingStatements); + if (Array.isArray(statements)) { + precedingStatements.push(...statements); + } else { + precedingStatements.push(statements); + } + } + + public prependPrecedingStatements(statements: lua.Statement | lua.Statement[]) { + const precedingStatements = this.precedingStatementsStack[this.precedingStatementsStack.length - 1]; + assert(precedingStatements); + if (Array.isArray(statements)) { + precedingStatements.unshift(...statements); + } else { + precedingStatements.unshift(statements); + } + } + + public createTempName(prefix = "temp") { + prefix = prefix.replace(/^_*/, ""); // Strip leading underscores because createSafeName will add them again + return createSafeName(`${prefix}_${this.nextTempId++}`); + } + + private getTempNameForLuaExpression(expression: lua.Expression): string | undefined { + if (lua.isStringLiteral(expression)) { + return expression.value; + } else if (lua.isNumericLiteral(expression)) { + return `_${expression.value.toString()}`; + } else if (lua.isIdentifier(expression)) { + return expression.text; + } else if (lua.isCallExpression(expression)) { + const name = this.getTempNameForLuaExpression(expression.expression); + if (name) { + return `${name}_result`; + } + } else if (lua.isTableIndexExpression(expression)) { + const tableName = this.getTempNameForLuaExpression(expression.table); + const indexName = this.getTempNameForLuaExpression(expression.index); + if (tableName || indexName) { + return `${tableName ?? "table"}_${indexName ?? "index"}`; + } + } + } + + public createTempNameForLuaExpression(expression: lua.Expression) { + const name = this.getTempNameForLuaExpression(expression); + const identifier = lua.createIdentifier(this.createTempName(name), undefined, tempSymbolId); + lua.setNodePosition(identifier, lua.getOriginalPos(expression)); + return identifier; + } + + private getTempNameForNode(node: ts.Node): string | undefined { + if (ts.isStringLiteral(node) || ts.isIdentifier(node) || ts.isMemberName(node)) { + return node.text; + } else if (ts.isNumericLiteral(node)) { + return `_${node.text}`; + } else if (ts.isCallExpression(node)) { + const name = this.getTempNameForNode(node.expression); + if (name) { + return `${name}_result`; + } + } else if (ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node)) { + const tableName = this.getTempNameForNode(node.expression); + const indexName = ts.isElementAccessExpression(node) + ? this.getTempNameForNode(node.argumentExpression) + : node.name.text; + if (tableName || indexName) { + return `${tableName ?? "table"}_${indexName ?? "index"}`; + } + } + } + + public createTempNameForNode(node: ts.Node) { + const name = this.getTempNameForNode(node); + return lua.createIdentifier(this.createTempName(name), node, tempSymbolId); + } + + // other utils + + private lastSymbolId = 0; + public readonly symbolInfoMap = new Map(); + public readonly symbolIdMaps = new Map(); + + public nextSymbolId(): lua.SymbolId { + return ++this.lastSymbolId as lua.SymbolId; + } + + public readonly usedLuaLibFeatures = new Set(); + + public readonly scopeStack: Scope[] = []; + private lastScopeId = 0; + + public pushScope(type: ScopeType, node: ts.Node): Scope { + const scope: Scope = { type, id: ++this.lastScopeId, node }; + this.scopeStack.push(scope); + return scope; + } + + public popScope(): Scope { + const scope = this.scopeStack.pop(); + assert(scope); + return scope; + } + + // Static context -> namespace dictionary keeping the current namespace for each transformation context + // see visitors/namespace.ts + /** @internal */ + public currentNamespaces: ts.ModuleDeclaration | undefined; + + /** @internal */ + public classSuperInfos: ClassSuperInfo[] = []; +} diff --git a/src/transformation/context/index.ts b/src/transformation/context/index.ts new file mode 100644 index 000000000..349cb7332 --- /dev/null +++ b/src/transformation/context/index.ts @@ -0,0 +1,2 @@ +export * from "./context"; +export * from "./visitors"; diff --git a/src/transformation/context/visitors.ts b/src/transformation/context/visitors.ts new file mode 100644 index 000000000..429cce1b4 --- /dev/null +++ b/src/transformation/context/visitors.ts @@ -0,0 +1,167 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { OneToManyVisitorResult } from "../utils/lua-ast"; +import { TransformationContext } from "./context"; + +interface NodesBySyntaxKind { + // Copied from is* type guards, with JSDoc and TypeNodes removed + [ts.SyntaxKind.NumericLiteral]: ts.NumericLiteral; + [ts.SyntaxKind.BigIntLiteral]: ts.BigIntLiteral; + [ts.SyntaxKind.StringLiteral]: ts.StringLiteral; + [ts.SyntaxKind.JsxText]: ts.JsxText; + [ts.SyntaxKind.RegularExpressionLiteral]: ts.RegularExpressionLiteral; + [ts.SyntaxKind.NoSubstitutionTemplateLiteral]: ts.NoSubstitutionTemplateLiteral; + [ts.SyntaxKind.TemplateHead]: ts.TemplateHead; + [ts.SyntaxKind.TemplateMiddle]: ts.TemplateMiddle; + [ts.SyntaxKind.TemplateTail]: ts.TemplateTail; + [ts.SyntaxKind.Identifier]: ts.Identifier; + [ts.SyntaxKind.QualifiedName]: ts.QualifiedName; + [ts.SyntaxKind.ComputedPropertyName]: ts.ComputedPropertyName; + [ts.SyntaxKind.TypeParameter]: ts.TypeParameterDeclaration; + [ts.SyntaxKind.Parameter]: ts.ParameterDeclaration; + [ts.SyntaxKind.Decorator]: ts.Decorator; + [ts.SyntaxKind.PropertySignature]: ts.PropertySignature; + [ts.SyntaxKind.PropertyDeclaration]: ts.PropertyDeclaration; + [ts.SyntaxKind.MethodSignature]: ts.MethodSignature; + [ts.SyntaxKind.MethodDeclaration]: ts.MethodDeclaration; + [ts.SyntaxKind.Constructor]: ts.ConstructorDeclaration; + [ts.SyntaxKind.GetAccessor]: ts.GetAccessorDeclaration; + [ts.SyntaxKind.SetAccessor]: ts.SetAccessorDeclaration; + [ts.SyntaxKind.CallSignature]: ts.CallSignatureDeclaration; + [ts.SyntaxKind.ConstructSignature]: ts.ConstructSignatureDeclaration; + [ts.SyntaxKind.IndexSignature]: ts.IndexSignatureDeclaration; + [ts.SyntaxKind.ObjectBindingPattern]: ts.ObjectBindingPattern; + [ts.SyntaxKind.ArrayBindingPattern]: ts.ArrayBindingPattern; + [ts.SyntaxKind.BindingElement]: ts.BindingElement; + [ts.SyntaxKind.ArrayLiteralExpression]: ts.ArrayLiteralExpression; + [ts.SyntaxKind.ObjectLiteralExpression]: ts.ObjectLiteralExpression; + [ts.SyntaxKind.PropertyAccessExpression]: ts.PropertyAccessExpression; + [ts.SyntaxKind.ElementAccessExpression]: ts.ElementAccessExpression; + [ts.SyntaxKind.CallExpression]: ts.CallExpression; + [ts.SyntaxKind.NewExpression]: ts.NewExpression; + [ts.SyntaxKind.TaggedTemplateExpression]: ts.TaggedTemplateExpression; + [ts.SyntaxKind.TypeAssertionExpression]: ts.TypeAssertion; + [ts.SyntaxKind.ParenthesizedExpression]: ts.ParenthesizedExpression; + [ts.SyntaxKind.FunctionExpression]: ts.FunctionExpression; + [ts.SyntaxKind.ArrowFunction]: ts.ArrowFunction; + [ts.SyntaxKind.DeleteExpression]: ts.DeleteExpression; + [ts.SyntaxKind.TypeOfExpression]: ts.TypeOfExpression; + [ts.SyntaxKind.VoidExpression]: ts.VoidExpression; + [ts.SyntaxKind.AwaitExpression]: ts.AwaitExpression; + [ts.SyntaxKind.PrefixUnaryExpression]: ts.PrefixUnaryExpression; + [ts.SyntaxKind.PostfixUnaryExpression]: ts.PostfixUnaryExpression; + [ts.SyntaxKind.BinaryExpression]: ts.BinaryExpression; + [ts.SyntaxKind.ConditionalExpression]: ts.ConditionalExpression; + [ts.SyntaxKind.TemplateExpression]: ts.TemplateExpression; + [ts.SyntaxKind.YieldExpression]: ts.YieldExpression; + [ts.SyntaxKind.SpreadElement]: ts.SpreadElement; + [ts.SyntaxKind.ClassExpression]: ts.ClassExpression; + [ts.SyntaxKind.OmittedExpression]: ts.OmittedExpression; + [ts.SyntaxKind.ExpressionWithTypeArguments]: ts.ExpressionWithTypeArguments; + [ts.SyntaxKind.AsExpression]: ts.AsExpression; + [ts.SyntaxKind.NonNullExpression]: ts.NonNullExpression; + [ts.SyntaxKind.MetaProperty]: ts.MetaProperty; + [ts.SyntaxKind.SatisfiesExpression]: ts.SatisfiesExpression; + [ts.SyntaxKind.TemplateSpan]: ts.TemplateSpan; + [ts.SyntaxKind.SemicolonClassElement]: ts.SemicolonClassElement; + [ts.SyntaxKind.Block]: ts.Block; + [ts.SyntaxKind.VariableStatement]: ts.VariableStatement; + [ts.SyntaxKind.EmptyStatement]: ts.EmptyStatement; + [ts.SyntaxKind.ExpressionStatement]: ts.ExpressionStatement; + [ts.SyntaxKind.IfStatement]: ts.IfStatement; + [ts.SyntaxKind.DoStatement]: ts.DoStatement; + [ts.SyntaxKind.WhileStatement]: ts.WhileStatement; + [ts.SyntaxKind.ForStatement]: ts.ForStatement; + [ts.SyntaxKind.ForInStatement]: ts.ForInStatement; + [ts.SyntaxKind.ForOfStatement]: ts.ForOfStatement; + [ts.SyntaxKind.ContinueStatement]: ts.ContinueStatement; + [ts.SyntaxKind.BreakStatement]: ts.BreakStatement; + [ts.SyntaxKind.ReturnStatement]: ts.ReturnStatement; + [ts.SyntaxKind.WithStatement]: ts.WithStatement; + [ts.SyntaxKind.SwitchStatement]: ts.SwitchStatement; + [ts.SyntaxKind.LabeledStatement]: ts.LabeledStatement; + [ts.SyntaxKind.ThrowStatement]: ts.ThrowStatement; + [ts.SyntaxKind.TryStatement]: ts.TryStatement; + [ts.SyntaxKind.DebuggerStatement]: ts.DebuggerStatement; + [ts.SyntaxKind.VariableDeclaration]: ts.VariableDeclaration; + [ts.SyntaxKind.VariableDeclarationList]: ts.VariableDeclarationList; + [ts.SyntaxKind.FunctionDeclaration]: ts.FunctionDeclaration; + [ts.SyntaxKind.ClassDeclaration]: ts.ClassDeclaration; + [ts.SyntaxKind.InterfaceDeclaration]: ts.InterfaceDeclaration; + [ts.SyntaxKind.TypeAliasDeclaration]: ts.TypeAliasDeclaration; + [ts.SyntaxKind.EnumDeclaration]: ts.EnumDeclaration; + [ts.SyntaxKind.ModuleDeclaration]: ts.ModuleDeclaration; + [ts.SyntaxKind.ModuleBlock]: ts.ModuleBlock; + [ts.SyntaxKind.CaseBlock]: ts.CaseBlock; + [ts.SyntaxKind.NamespaceExportDeclaration]: ts.NamespaceExportDeclaration; + [ts.SyntaxKind.ImportEqualsDeclaration]: ts.ImportEqualsDeclaration; + [ts.SyntaxKind.ImportDeclaration]: ts.ImportDeclaration; + [ts.SyntaxKind.ImportClause]: ts.ImportClause; + [ts.SyntaxKind.NamespaceImport]: ts.NamespaceImport; + [ts.SyntaxKind.NamedImports]: ts.NamedImports; + [ts.SyntaxKind.ImportSpecifier]: ts.ImportSpecifier; + [ts.SyntaxKind.ExportAssignment]: ts.ExportAssignment; + [ts.SyntaxKind.ExportDeclaration]: ts.ExportDeclaration; + [ts.SyntaxKind.NamedExports]: ts.NamedExports; + [ts.SyntaxKind.ExportSpecifier]: ts.ExportSpecifier; + [ts.SyntaxKind.MissingDeclaration]: ts.MissingDeclaration; + [ts.SyntaxKind.ExternalModuleReference]: ts.ExternalModuleReference; + [ts.SyntaxKind.JsxElement]: ts.JsxElement; + [ts.SyntaxKind.JsxSelfClosingElement]: ts.JsxSelfClosingElement; + [ts.SyntaxKind.JsxOpeningElement]: ts.JsxOpeningElement; + [ts.SyntaxKind.JsxClosingElement]: ts.JsxClosingElement; + [ts.SyntaxKind.JsxFragment]: ts.JsxFragment; + [ts.SyntaxKind.JsxOpeningFragment]: ts.JsxOpeningFragment; + [ts.SyntaxKind.JsxClosingFragment]: ts.JsxClosingFragment; + [ts.SyntaxKind.JsxAttribute]: ts.JsxAttribute; + [ts.SyntaxKind.JsxAttributes]: ts.JsxAttributes; + [ts.SyntaxKind.JsxSpreadAttribute]: ts.JsxSpreadAttribute; + [ts.SyntaxKind.JsxExpression]: ts.JsxExpression; + [ts.SyntaxKind.CaseClause]: ts.CaseClause; + [ts.SyntaxKind.DefaultClause]: ts.DefaultClause; + [ts.SyntaxKind.HeritageClause]: ts.HeritageClause; + [ts.SyntaxKind.CatchClause]: ts.CatchClause; + [ts.SyntaxKind.PropertyAssignment]: ts.PropertyAssignment; + [ts.SyntaxKind.ShorthandPropertyAssignment]: ts.ShorthandPropertyAssignment; + [ts.SyntaxKind.SpreadAssignment]: ts.SpreadAssignment; + [ts.SyntaxKind.EnumMember]: ts.EnumMember; + [ts.SyntaxKind.SourceFile]: ts.SourceFile; + // [ts.SyntaxKind.Bundle]: ts.Bundle; + + // Not included above + [ts.SyntaxKind.TrueKeyword]: ts.BooleanLiteral; + [ts.SyntaxKind.FalseKeyword]: ts.BooleanLiteral; + [ts.SyntaxKind.NullKeyword]: ts.NullLiteral; + [ts.SyntaxKind.SuperKeyword]: ts.SuperExpression; + [ts.SyntaxKind.ThisKeyword]: ts.ThisExpression; + [ts.SyntaxKind.NotEmittedStatement]: ts.NotEmittedStatement; +} + +export type ExpressionLikeNode = ts.Expression | ts.QualifiedName | ts.ExternalModuleReference; +export type StatementLikeNode = ts.Statement; +export type VisitorResult = T extends ExpressionLikeNode + ? lua.Expression + : T extends StatementLikeNode + ? OneToManyVisitorResult + : T extends ts.SourceFile + ? lua.File + : OneToManyVisitorResult; + +export type Visitor = FunctionVisitor | ObjectVisitor; +export type FunctionVisitor = (node: T, context: TransformationContext) => VisitorResult; + +export interface ObjectVisitor { + transform: FunctionVisitor; + + /** + * Visitors with higher priority are called first. + * + * Higher-priority visitors can call lower ones with `context.superTransformNode`. + * + * Standard visitors have the lowest (`-Infinity`) priority. + */ + priority?: number; +} + +export type Visitors = { [P in keyof NodesBySyntaxKind]?: Visitor }; +export type VisitorMap = Map>>; diff --git a/src/transformation/index.ts b/src/transformation/index.ts new file mode 100644 index 000000000..b9e1f6241 --- /dev/null +++ b/src/transformation/index.ts @@ -0,0 +1,45 @@ +import * as ts from "typescript"; +import * as lua from "../LuaAST"; +import { getOrUpdate } from "../utils"; +import { ObjectVisitor, TransformationContext, VisitorMap, Visitors } from "./context"; +import { standardVisitors } from "./visitors"; +import { usingTransformer } from "./pre-transformers/using-transformer"; + +export function createVisitorMap(customVisitors: Visitors[]): VisitorMap { + const objectVisitorMap: Map>> = new Map(); + for (const visitors of [standardVisitors, ...customVisitors]) { + const priority = visitors === standardVisitors ? -Infinity : 0; + for (const [syntaxKindKey, visitor] of Object.entries(visitors)) { + if (!visitor) continue; + + const syntaxKind = Number(syntaxKindKey) as ts.SyntaxKind; + const nodeVisitors = getOrUpdate(objectVisitorMap, syntaxKind, () => []); + + const objectVisitor: ObjectVisitor = + typeof visitor === "function" ? { transform: visitor, priority } : visitor; + nodeVisitors.push(objectVisitor); + } + } + + const result: VisitorMap = new Map(); + for (const [kind, nodeVisitors] of objectVisitorMap) { + result.set( + kind, + nodeVisitors.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)).map(visitor => visitor.transform) + ); + } + return result; +} + +export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) { + const context = new TransformationContext(program, sourceFile, visitorMap); + + // TS -> TS pre-transformation + const preTransformers = [usingTransformer(context)]; + const result = ts.transform(sourceFile, preTransformers); + + // TS -> Lua transformation + const [file] = context.transformNode(result.transformed[0]) as [lua.File]; + + return { file, diagnostics: context.diagnostics }; +} diff --git a/src/transformation/pre-transformers/using-transformer.ts b/src/transformation/pre-transformers/using-transformer.ts new file mode 100644 index 000000000..1229b71c2 --- /dev/null +++ b/src/transformation/pre-transformers/using-transformer.ts @@ -0,0 +1,139 @@ +import * as ts from "typescript"; +import { TransformationContext } from "../context"; +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) || 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.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 + const parent: ts.Node[] = [updatedBlock]; + function setParent(node2: ts.Node): ts.Node { + ts.setParent(node2, parent[parent.length - 1]); + parent.push(node2); + ts.visitEachChild(node2, setParent, ctx); + parent.pop(); + return node2; + } + ts.visitEachChild(updatedBlock, setParent, ctx); + ts.setParent(updatedBlock, node.parent); + + return result; + } + } + return ts.visitEachChild(node, visit, ctx); + } + const transformedSourceFile = ts.visitEachChild(sourceFile, visit, ctx); + return visit(transformedSourceFile) as ts.SourceFile; + }; +} + +function isUsingDeclarationList(node: ts.Node): node is ts.VariableStatement { + return ts.isVariableStatement(node) && (node.declarationList.flags & ts.NodeFlags.Using) !== 0; +} + +function transformBlockWithUsing( + context: TransformationContext, + statements: ts.NodeArray | ts.Statement[], + block: ts.Block | ts.SourceFile +): [true, ts.Statement[]] | [false] { + const newStatements: ts.Statement[] = []; + + for (let i = 0; i < statements.length; i++) { + const statement = statements[i]; + if (isUsingDeclarationList(statement)) { + const isAwaitUsing = (statement.declarationList.flags & ts.NodeFlags.AwaitContext) !== 0; + + if (isAwaitUsing) { + importLuaLibFeature(context, LuaLibFeature.UsingAsync); + } else { + importLuaLibFeature(context, LuaLibFeature.Using); + } + + // Make declared using variables callback function parameters + const variableNames = statement.declarationList.declarations.map(d => + ts.factory.createParameterDeclaration(undefined, undefined, d.name) + ); + // Add this: void as first parameter + variableNames.unshift(createThisVoidParameter(context.checker)); + + // Put all following statements in the callback body + const followingStatements = statements.slice(i + 1); + const [followingHasUsings, replacedFollowingStatements] = transformBlockWithUsing( + context, + followingStatements, + block + ); + const callbackBody = ts.factory.createBlock( + followingHasUsings ? replacedFollowingStatements : followingStatements + ); + + const callback = ts.factory.createFunctionExpression( + // Put async keyword in front of callback when we are in an async using + isAwaitUsing ? [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)] : undefined, + undefined, + undefined, + undefined, + variableNames, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), // Required for TS to not freak out trying to infer the type of synthetic nodes + callbackBody + ); + + // Replace using variable list with call to lualib function with callback and followed by all variable initializers + const functionIdentifier = ts.factory.createIdentifier(isAwaitUsing ? "__TS__UsingAsync" : "__TS__Using"); + let call: ts.Expression = ts.factory.createCallExpression( + functionIdentifier, + [], + [ + callback, + ...statement.declarationList.declarations.map( + d => d.initializer ?? ts.factory.createIdentifier("unidentified") + ), + ] + ); + + // If this is an 'await using ...', add an await statement here + if (isAwaitUsing) { + call = ts.factory.createAwaitExpression(call); + } + + 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 { + newStatements.push(ts.factory.createReturnStatement(call)); + } + + return [true, newStatements]; + } else { + newStatements.push(statement); + } + } + return [false]; +} + +function createThisVoidParameter(checker: ts.TypeChecker) { + const voidType = checker.typeToTypeNode(checker.getVoidType(), undefined, undefined); + return ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("this"), + undefined, + voidType + ); +} diff --git a/src/transformation/utils/annotations.ts b/src/transformation/utils/annotations.ts new file mode 100644 index 000000000..b66704c97 --- /dev/null +++ b/src/transformation/utils/annotations.ts @@ -0,0 +1,116 @@ +import * as ts from "typescript"; + +export enum AnnotationKind { + CustomConstructor = "customConstructor", + CompileMembersOnly = "compileMembersOnly", + NoResolution = "noResolution", + NoSelf = "noSelf", + CustomName = "customName", + NoSelfInFile = "noSelfInFile", +} + +const annotationValues = new Map(Object.values(AnnotationKind).map(k => [k.toLowerCase(), k])); + +export interface Annotation { + kind: AnnotationKind; + args: string[]; +} + +export type AnnotationsMap = Map; + +function collectAnnotations(source: ts.Symbol | ts.Signature, annotationsMap: AnnotationsMap): void { + for (const tag of source.getJsDocTags()) { + const tagName = annotationValues.get(tag.name.toLowerCase()); + if (!tagName) continue; + const annotation: Annotation = { + kind: tag.name as AnnotationKind, + args: tag.text?.map(p => p.text) ?? [], + }; + annotationsMap.set(tagName, annotation); + } +} + +const symbolAnnotations = new WeakMap(); + +export function getSymbolAnnotations(symbol: ts.Symbol): AnnotationsMap { + const known = symbolAnnotations.get(symbol); + if (known) return known; + + const annotationsMap: AnnotationsMap = new Map(); + collectAnnotations(symbol, annotationsMap); + + symbolAnnotations.set(symbol, annotationsMap); + return annotationsMap; +} + +export function getTypeAnnotations(type: ts.Type): AnnotationsMap { + // types are not frequently repeatedly polled for annotations, so it's not worth caching them + const annotationsMap: AnnotationsMap = new Map(); + if (type.symbol) { + getSymbolAnnotations(type.symbol).forEach((value, key) => { + annotationsMap.set(key, value); + }); + } + if (type.aliasSymbol) { + getSymbolAnnotations(type.aliasSymbol).forEach((value, key) => { + annotationsMap.set(key, value); + }); + } + return annotationsMap; +} + +const nodeAnnotations = new WeakMap(); +export function getNodeAnnotations(node: ts.Node): AnnotationsMap { + const known = nodeAnnotations.get(node); + if (known) return known; + + const annotationsMap: AnnotationsMap = new Map(); + collectAnnotationsFromTags(annotationsMap, ts.getAllJSDocTags(node, ts.isJSDocUnknownTag)); + + nodeAnnotations.set(node, annotationsMap); + return annotationsMap; +} + +function collectAnnotationsFromTags(annotationsMap: AnnotationsMap, tags: readonly ts.JSDocTag[]) { + for (const tag of tags) { + const tagName = annotationValues.get(tag.tagName.text.toLowerCase()); + if (!tagName) continue; + annotationsMap.set(tagName, { kind: tagName, args: getTagArgsFromComment(tag) }); + } +} + +const fileAnnotations = new WeakMap(); +export function getFileAnnotations(sourceFile: ts.SourceFile): AnnotationsMap { + const known = fileAnnotations.get(sourceFile); + if (known) return known; + + const annotationsMap: AnnotationsMap = new Map(); + + if (sourceFile.statements.length > 0) { + // Manually collect jsDoc because `getJSDocTags` includes tags only from closest comment + const jsDoc = sourceFile.statements[0].jsDoc; + if (jsDoc) { + for (const jsDocElement of jsDoc) { + if (jsDocElement.tags) { + collectAnnotationsFromTags(annotationsMap, jsDocElement.tags); + } + } + } + } + + fileAnnotations.set(sourceFile, annotationsMap); + return annotationsMap; +} + +function getTagArgsFromComment(tag: ts.JSDocTag): string[] { + if (tag.comment) { + if (typeof tag.comment === "string") { + const firstLine = tag.comment.split("\n")[0]; + return firstLine.trim().split(" "); + } else { + return tag.comment.map(part => part.text); + } + } + + return []; +} diff --git a/src/transformation/utils/assignment-validation.ts b/src/transformation/utils/assignment-validation.ts new file mode 100644 index 000000000..d0f42d90a --- /dev/null +++ b/src/transformation/utils/assignment-validation.ts @@ -0,0 +1,111 @@ +import * as ts from "typescript"; +import { getOrUpdate } from "../../utils"; +import { TransformationContext } from "../context"; +import { + unsupportedNoSelfFunctionConversion, + unsupportedOverloadAssignment, + unsupportedSelfFunctionConversion, +} from "./diagnostics"; +import { ContextType, getFunctionContextType } from "./function-context"; + +// TODO: Clear if types are reused between compilations +const typeValidationCache = new WeakMap>(); + +export function validateAssignment( + context: TransformationContext, + node: ts.Node, + fromType: ts.Type, + toType: ts.Type, + toName?: string +): void { + if (toType === fromType) { + return; + } + + if ((toType.flags & ts.TypeFlags.Any) !== 0) { + // Assigning to un-typed variable + return; + } + + // Use cache to avoid repeating check for same types (protects against infinite loop in recursive types) + const fromTypeCache = getOrUpdate(typeValidationCache, fromType, () => new Set()); + if (fromTypeCache.has(toType)) return; + fromTypeCache.add(toType); + + validateFunctionAssignment(context, node, fromType, toType, toName); + + const checker = context.checker; + if ( + (checker.isTupleType(toType) || checker.isArrayType(toType)) && + (checker.isTupleType(fromType) || checker.isArrayType(fromType)) + ) { + // Recurse into arrays/tuples + const fromTypeArguments = (fromType as ts.TypeReference).typeArguments; + const toTypeArguments = (toType as ts.TypeReference).typeArguments; + + if (fromTypeArguments === undefined || toTypeArguments === undefined) { + return; + } + + const count = Math.min(fromTypeArguments.length, toTypeArguments.length); + for (let i = 0; i < count; ++i) { + validateAssignment(context, node, fromTypeArguments[i], toTypeArguments[i], toName); + } + } + + const fromMembers = fromType.symbol?.members; + const toMembers = toType.symbol?.members; + + if (fromMembers && toMembers) { + // Recurse into interfaces + if (toMembers.size < fromMembers.size) { + toMembers.forEach((toMember, escapedMemberName) => { + const fromMember = fromMembers.get(escapedMemberName); + if (fromMember) { + validateMember(toMember, fromMember, escapedMemberName); + } + }); + } else { + fromMembers.forEach((fromMember, escapedMemberName) => { + const toMember = toMembers.get(escapedMemberName); + if (toMember) { + validateMember(toMember, fromMember, escapedMemberName); + } + }); + } + } + + function validateMember(toMember: ts.Symbol, fromMember: ts.Symbol, escapedMemberName: ts.__String): void { + const toMemberType = context.checker.getTypeOfSymbolAtLocation(toMember, node); + const fromMemberType = context.checker.getTypeOfSymbolAtLocation(fromMember, node); + const memberName = ts.unescapeLeadingUnderscores(escapedMemberName); + validateAssignment( + context, + node, + fromMemberType, + toMemberType, + toName ? `${toName}.${memberName}` : memberName + ); + } +} + +function validateFunctionAssignment( + context: TransformationContext, + node: ts.Node, + fromType: ts.Type, + toType: ts.Type, + toName?: string +): void { + const fromContext = getFunctionContextType(context, fromType); + const toContext = getFunctionContextType(context, toType); + + if (fromContext === ContextType.Mixed || toContext === ContextType.Mixed) { + context.diagnostics.push(unsupportedOverloadAssignment(node, toName)); + } else if (fromContext !== toContext && fromContext !== ContextType.None && toContext !== ContextType.None) { + context.diagnostics.push( + toContext === ContextType.Void + ? unsupportedNoSelfFunctionConversion(node, toName) + : unsupportedSelfFunctionConversion(node, toName) + ); + } +} diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts new file mode 100644 index 000000000..5fb1bf15e --- /dev/null +++ b/src/transformation/utils/diagnostics.ts @@ -0,0 +1,178 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { LuaTarget, TypeScriptToLuaOptions } from "../../CompilerOptions"; +import { createSerialDiagnosticFactory } from "../../utils"; +import { AnnotationKind } from "./annotations"; + +type MessageProvider = string | ((...args: TArgs) => string); + +const createDiagnosticFactory = ( + category: ts.DiagnosticCategory, + message: MessageProvider +) => + createSerialDiagnosticFactory((node: ts.Node, ...args: TArgs) => ({ + file: ts.getOriginalNode(node).getSourceFile(), + start: ts.getOriginalNode(node).getStart(), + length: ts.getOriginalNode(node).getWidth(), + messageText: typeof message === "string" ? message : message(...args), + category, + })); + +const createErrorDiagnosticFactory = (message: MessageProvider) => + createDiagnosticFactory(ts.DiagnosticCategory.Error, message); +const createWarningDiagnosticFactory = (message: MessageProvider) => + createDiagnosticFactory(ts.DiagnosticCategory.Warning, message); + +export const unsupportedNodeKind = createErrorDiagnosticFactory( + (kind: ts.SyntaxKind) => `Unsupported node kind ${ts.SyntaxKind[kind]}` +); + +export const forbiddenForIn = createErrorDiagnosticFactory("Iterating over arrays with 'for ... in' is not allowed."); + +export const unsupportedNoSelfFunctionConversion = createErrorDiagnosticFactory((name?: string) => { + const nameReference = name ? ` '${name}'` : ""; + return ( + `Unable to convert function with a 'this' parameter to function${nameReference} with no 'this'. ` + + "To fix, wrap in an arrow function, or declare with 'this: void'." + ); +}); + +export const unsupportedSelfFunctionConversion = createErrorDiagnosticFactory((name?: string) => { + const nameReference = name ? ` '${name}'` : ""; + return ( + `Unable to convert function with no 'this' parameter to function${nameReference} with 'this'. ` + + "To fix, wrap in an arrow function, or declare with 'this: any'." + ); +}); + +export const unsupportedOverloadAssignment = createErrorDiagnosticFactory((name?: string) => { + const nameReference = name ? ` to '${name}'` : ""; + return ( + `Unsupported assignment of function with different overloaded types for 'this'${nameReference}. ` + + "Overloads should all have the same type for 'this'." + ); +}); + +export const decoratorInvalidContext = createErrorDiagnosticFactory("Decorator function cannot have 'this: void'."); + +export const annotationInvalidArgumentCount = createErrorDiagnosticFactory( + (kind: AnnotationKind, got: number, expected: number) => `'@${kind}' expects ${expected} arguments, but got ${got}.` +); + +export const invalidRangeUse = createErrorDiagnosticFactory("$range can only be used in a for...of loop."); + +export const invalidVarargUse = createErrorDiagnosticFactory( + "$vararg can only be used in a spread element ('...$vararg') in global scope." +); + +export const invalidRangeControlVariable = createErrorDiagnosticFactory( + "For loop using $range must declare a single control variable." +); + +export const invalidMultiIterableWithoutDestructuring = createErrorDiagnosticFactory( + "LuaIterable with a LuaMultiReturn return value type must be destructured." +); + +export const invalidPairsIterableWithoutDestructuring = createErrorDiagnosticFactory( + "LuaPairsIterable type must be destructured in a for...of statement." +); + +export const unsupportedAccessorInObjectLiteral = createErrorDiagnosticFactory( + "Accessors in object literal are not supported." +); + +export const unsupportedRightShiftOperator = createErrorDiagnosticFactory( + "Right shift operator is not supported for target Lua 5.3. Use `>>>` instead." +); + +const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? "LuaJIT" : `Lua ${version}`); +export const unsupportedForTarget = createErrorDiagnosticFactory( + (functionality: string, version: LuaTarget) => + `${functionality} is/are not supported for target ${getLuaTargetName(version)}.` +); + +export const unsupportedForTargetButOverrideAvailable = createErrorDiagnosticFactory( + (functionality: string, version: LuaTarget, optionName: keyof TypeScriptToLuaOptions) => + `As a precaution, ${functionality} is/are not supported for target ${getLuaTargetName( + version + )} due to language features/limitations. ` + + `However "--${optionName}" can be used to bypass this precaution. ` + + "See https://typescripttolua.github.io/docs/configuration for more information." +); + +export const unsupportedProperty = createErrorDiagnosticFactory( + (parentName: string, property: string) => `${parentName}.${property} is unsupported.` +); + +export const invalidAmbientIdentifierName = createErrorDiagnosticFactory( + (text: string) => `Invalid ambient identifier name '${text}'. Ambient identifiers must be valid lua identifiers.` +); + +export const unsupportedVarDeclaration = createErrorDiagnosticFactory( + "`var` declarations are not supported. Use `let` or `const` instead." +); + +export const invalidMultiFunctionUse = createErrorDiagnosticFactory( + "The $multi function must be called in a return statement." +); + +export const invalidMultiFunctionReturnType = createErrorDiagnosticFactory( + "The $multi function cannot be cast to a non-LuaMultiReturn type." +); + +export const invalidMultiReturnAccess = createErrorDiagnosticFactory( + "The LuaMultiReturn type can only be accessed via an element access expression of a numeric type." +); + +export const invalidCallExtensionUse = createErrorDiagnosticFactory( + "This function must be called directly and cannot be referred to." +); +export const annotationDeprecated = createWarningDiagnosticFactory( + (kind: AnnotationKind) => + `'@${kind}' is deprecated and will be removed in a future update. Please update your code before upgrading to the next release, otherwise your project will no longer compile. ` + + `See https://typescripttolua.github.io/docs/advanced/compiler-annotations#${kind.toLowerCase()} for more information.` +); + +export const truthyOnlyConditionalValue = createWarningDiagnosticFactory( + "Only false and nil evaluate to 'false' in Lua, everything else is considered 'true'. Explicitly compare the value with ===." +); + +export const notAllowedOptionalAssignment = createErrorDiagnosticFactory( + "The left-hand side of an assignment expression may not be an optional property access." +); + +export const awaitMustBeInAsyncFunction = createErrorDiagnosticFactory( + "Await can only be used inside async functions." +); + +export const unsupportedBuiltinOptionalCall = createErrorDiagnosticFactory( + "Optional calls are not supported for builtin or language extension functions." +); + +export const unsupportedOptionalCompileMembersOnly = createErrorDiagnosticFactory( + "Optional calls are not supported on enums marked with @compileMembersOnly." +); + +export const undefinedInArrayLiteral = createErrorDiagnosticFactory( + "Array literals may not contain undefined or null." +); + +export const invalidMethodCallExtensionUse = createErrorDiagnosticFactory( + "This language extension must be called as a method." +); + +export const invalidSpreadInCallExtension = createErrorDiagnosticFactory( + "Spread elements are not supported in call extensions." +); + +export const cannotAssignToNodeOfKind = createErrorDiagnosticFactory( + (kind: lua.SyntaxKind) => `Cannot create assignment assigning to a node of type ${lua.SyntaxKind[kind]}.` +); + +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 new file mode 100644 index 000000000..f0ef437e5 --- /dev/null +++ b/src/transformation/utils/export.ts @@ -0,0 +1,163 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { createModuleLocalName } from "../visitors/namespace"; +import { createExportsIdentifier } from "./lua-ast"; +import { getSymbolInfo } from "./symbols"; +import { findFirstNodeAbove } from "./typescript"; + +export function hasDefaultExportModifier(node: ts.Node): boolean { + return ( + ts.canHaveModifiers(node) && + node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true + ); +} + +export function hasExportModifier(node: ts.Node): boolean { + return ( + ts.canHaveModifiers(node) && + node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) === true + ); +} + +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); + +export function getExportedSymbolDeclaration(symbol: ts.Symbol): ts.Declaration | undefined { + const declarations = symbol.getDeclarations(); + if (declarations) { + return declarations.find(d => (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Export) !== 0); + } +} + +export function getSymbolFromIdentifier( + context: TransformationContext, + identifier: lua.Identifier +): ts.Symbol | undefined { + if (identifier.symbolId !== undefined) { + const symbolInfo = getSymbolInfo(context, identifier.symbolId); + if (symbolInfo !== undefined) { + return symbolInfo.symbol; + } + } +} + +export function getIdentifierExportScope( + context: TransformationContext, + identifier: lua.Identifier +): ts.SourceFile | ts.ModuleDeclaration | undefined { + const symbol = getSymbolFromIdentifier(context, identifier); + if (!symbol) { + return undefined; + } + + return getSymbolExportScope(context, symbol); +} + +function isGlobalAugmentation(module: ts.ModuleDeclaration): boolean { + return (module.flags & ts.NodeFlags.GlobalAugmentation) !== 0; +} + +export function getSymbolExportScope( + context: TransformationContext, + symbol: ts.Symbol +): ts.SourceFile | ts.ModuleDeclaration | undefined { + const exportedDeclaration = getExportedSymbolDeclaration(symbol); + if (!exportedDeclaration) { + return undefined; + } + + const scope = findFirstNodeAbove( + exportedDeclaration, + (n): n is ts.SourceFile | ts.ModuleDeclaration => ts.isSourceFile(n) || ts.isModuleDeclaration(n) + ); + if (!scope) { + return undefined; + } + + if (ts.isModuleDeclaration(scope) && isGlobalAugmentation(scope)) { + return undefined; + } + + if (!isSymbolExportedFromScope(context, symbol, scope)) { + return undefined; + } + + return scope; +} + +export function getExportedSymbolsFromScope( + context: TransformationContext, + scope: ts.SourceFile | ts.ModuleDeclaration +): ts.Symbol[] { + const scopeSymbol = context.checker.getSymbolAtLocation(ts.isSourceFile(scope) ? scope : scope.name); + if (scopeSymbol?.exports === undefined) { + return []; + } + + // ts.Iterator is not a ES6-compatible iterator, because TypeScript targets ES5 + const it: Iterable = { [Symbol.iterator]: () => scopeSymbol.exports!.values() }; + return [...it]; +} + +export function getDependenciesOfSymbol(context: TransformationContext, originalSymbol: ts.Symbol): ts.Symbol[] { + return getExportedSymbolsFromScope(context, context.sourceFile).filter(exportSymbol => + exportSymbol.declarations + ?.filter(ts.isExportSpecifier) + .map(context.checker.getExportSpecifierLocalTargetSymbol) + .includes(originalSymbol) + ); +} + +export function isSymbolExported(context: TransformationContext, symbol: ts.Symbol): boolean { + return ( + getExportedSymbolDeclaration(symbol) !== undefined || + // Symbol may have been exported separately (e.g. 'const foo = "bar"; export { foo }') + isSymbolExportedFromScope(context, symbol, context.sourceFile) + ); +} + +export function isSymbolExportedFromScope( + context: TransformationContext, + symbol: ts.Symbol, + scope: ts.SourceFile | ts.ModuleDeclaration +): boolean { + return getExportedSymbolsFromScope(context, scope).includes(symbol); +} + +export function addExportToIdentifier( + context: TransformationContext, + identifier: lua.Identifier +): lua.AssignmentLeftHandSideExpression { + const exportScope = getIdentifierExportScope(context, identifier); + return exportScope ? createExportedIdentifier(context, identifier, exportScope) : identifier; +} + +export function createExportedIdentifier( + context: TransformationContext, + identifier: lua.Identifier, + exportScope?: ts.SourceFile | ts.ModuleDeclaration +): lua.AssignmentLeftHandSideExpression { + if (!identifier.exportable) { + return identifier; + } + + const exportTable = + exportScope && ts.isModuleDeclaration(exportScope) + ? createModuleLocalName(context, exportScope) + : createExportsIdentifier(); + + return lua.createTableIndexExpression(exportTable, lua.createStringLiteral(identifier.text)); +} + +export function createDefaultExportExpression(node: ts.Node): lua.AssignmentLeftHandSideExpression { + return lua.createTableIndexExpression(createExportsIdentifier(), createDefaultExportStringLiteral(node), node); +} diff --git a/src/transformation/utils/function-context.ts b/src/transformation/utils/function-context.ts new file mode 100644 index 000000000..d61ffb5bf --- /dev/null +++ b/src/transformation/utils/function-context.ts @@ -0,0 +1,238 @@ +import * as ts from "typescript"; +import { CompilerOptions } from "../../CompilerOptions"; +import { TransformationContext } from "../context"; +import { AnnotationKind, getFileAnnotations, getNodeAnnotations } from "./annotations"; +import { findFirstNodeAbove, getAllCallSignatures, inferAssignedType } from "./typescript"; + +export enum ContextType { + None = 0, + Void = 1 << 0, + NonVoid = 1 << 1, + Mixed = Void | NonVoid, +} + +function hasNoSelfAncestor(declaration: ts.Declaration): boolean { + const scopeDeclaration = findFirstNodeAbove( + declaration, + (node): node is ts.SourceFile | ts.ModuleDeclaration => ts.isSourceFile(node) || ts.isModuleDeclaration(node) + ); + + if (!scopeDeclaration) { + return false; + } else if (ts.isSourceFile(scopeDeclaration)) { + return getFileAnnotations(scopeDeclaration).has(AnnotationKind.NoSelfInFile); + } else if (getNodeAnnotations(scopeDeclaration).has(AnnotationKind.NoSelf)) { + return true; + } else { + return hasNoSelfAncestor(scopeDeclaration); + } +} + +function getExplicitThisParameter(signatureDeclaration: ts.SignatureDeclaration): ts.ParameterDeclaration | undefined { + const param = signatureDeclaration.parameters[0]; + if (param && ts.isIdentifier(param.name) && ts.identifierToKeywordKind(param.name) === ts.SyntaxKind.ThisKeyword) { + return param; + } +} + +const callContextTypes = new WeakMap(); + +export function getCallContextType(context: TransformationContext, callExpression: ts.CallLikeExpression): ContextType { + const known = callContextTypes.get(callExpression); + if (known !== undefined) return known; + + const signature = context.checker.getResolvedSignature(callExpression); + const signatureDeclaration = signature?.getDeclaration(); + + let contextType = ContextType.None; + + if (signatureDeclaration) { + contextType = computeDeclarationContextType(context, signatureDeclaration); + } else { + // No signature declaration could be resolved, so instead try to see if the declaration is in a + // noSelfInFile file + const declarations = findRootDeclarations(context, callExpression); + contextType = declarations.some(d => getFileAnnotations(d.getSourceFile()).has(AnnotationKind.NoSelfInFile)) + ? ContextType.Void + : context.options.noImplicitSelf + ? ContextType.Void + : ContextType.NonVoid; + } + + callContextTypes.set(callExpression, contextType); + return contextType; +} + +const signatureDeclarationContextTypes = new WeakMap(); + +function getSignatureContextType( + context: TransformationContext, + signatureDeclaration: ts.SignatureDeclaration +): ContextType { + const known = signatureDeclarationContextTypes.get(signatureDeclaration); + if (known !== undefined) return known; + const contextType = computeDeclarationContextType(context, signatureDeclaration); + signatureDeclarationContextTypes.set(signatureDeclaration, contextType); + return contextType; +} + +function findRootDeclarations(context: TransformationContext, callExpression: ts.CallLikeExpression): ts.Declaration[] { + const calledExpression = ts.isTaggedTemplateExpression(callExpression) + ? callExpression.tag + : ts.isJsxSelfClosingElement(callExpression) + ? callExpression.tagName + : ts.isJsxOpeningElement(callExpression) + ? callExpression.tagName + : ts.isCallExpression(callExpression) + ? callExpression.expression + : undefined; + + if (!calledExpression) return []; + + const calledSymbol = context.checker.getSymbolAtLocation(calledExpression); + if (calledSymbol === undefined) return []; + + return ( + calledSymbol.getDeclarations()?.flatMap(d => { + if (ts.isImportSpecifier(d)) { + const aliasSymbol = context.checker.getAliasedSymbol(calledSymbol); + return aliasSymbol.getDeclarations() ?? []; + } else { + return [d]; + } + }) ?? [] + ); +} + +function computeDeclarationContextType(context: TransformationContext, signatureDeclaration: ts.SignatureDeclaration) { + const thisParameter = getExplicitThisParameter(signatureDeclaration); + if (thisParameter) { + // Explicit 'this' + return thisParameter.type && thisParameter.type.kind === ts.SyntaxKind.VoidKeyword + ? ContextType.Void + : ContextType.NonVoid; + } + + // noSelf declaration on function signature + if (getNodeAnnotations(signatureDeclaration).has(AnnotationKind.NoSelf)) { + return ContextType.Void; + } + + if ( + ts.isMethodSignature(signatureDeclaration) || + ts.isMethodDeclaration(signatureDeclaration) || + ts.isConstructSignatureDeclaration(signatureDeclaration) || + ts.isConstructorDeclaration(signatureDeclaration) || + (signatureDeclaration.parent && ts.isPropertyDeclaration(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( + 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; + } + + return ContextType.NonVoid; + } + + if (signatureDeclaration.parent && ts.isTypeParameterDeclaration(signatureDeclaration.parent)) { + return ContextType.NonVoid; + } + + // 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; + if (options.noImplicitSelf) { + const sourceFile = program.getSourceFile(signatureDeclaration.getSourceFile().fileName); + if ( + sourceFile !== undefined && + !program.isSourceFileDefaultLibrary(sourceFile) && + !program.isSourceFileFromExternalLibrary(sourceFile) + ) { + return ContextType.Void; + } + } + + // Walk up to find @noSelf or @noSelfInFile + if (hasNoSelfAncestor(signatureDeclaration)) { + return ContextType.Void; + } + + return ContextType.NonVoid; +} + +function reduceContextTypes(contexts: ContextType[]): ContextType { + let type = ContextType.None; + for (const context of contexts) { + type |= context; + if (type === ContextType.Mixed) break; + } + return type; +} + +function getSignatureDeclarations(context: TransformationContext, signature: ts.Signature): ts.SignatureDeclaration[] { + if (signature.compositeSignatures) { + return signature.compositeSignatures.flatMap(s => getSignatureDeclarations(context, s)); + } + + const signatureDeclaration = signature.getDeclaration(); + if (signatureDeclaration === undefined) { + return []; + } + + let inferredType: ts.Type | undefined; + if ( + ts.isMethodDeclaration(signatureDeclaration) && + ts.isObjectLiteralExpression(signatureDeclaration.parent) && + !getExplicitThisParameter(signatureDeclaration) + ) { + inferredType = context.checker.getContextualTypeForObjectLiteralElement(signatureDeclaration); + } else if ( + (ts.isFunctionExpression(signatureDeclaration) || ts.isArrowFunction(signatureDeclaration)) && + !getExplicitThisParameter(signatureDeclaration) + ) { + // Infer type of function expressions/arrow functions + inferredType = inferAssignedType(context, signatureDeclaration); + } + + if (inferredType) { + const inferredSignatures = getAllCallSignatures(inferredType); + if (inferredSignatures.length > 0) { + return inferredSignatures.map(s => s.getDeclaration()); + } + } + + return [signatureDeclaration]; +} + +const typeContextTypes = new WeakMap(); + +export function getFunctionContextType(context: TransformationContext, type: ts.Type): ContextType { + const known = typeContextTypes.get(type); + if (known !== undefined) return known; + const contextType = computeFunctionContextType(context, type); + typeContextTypes.set(type, contextType); + return contextType; +} + +function computeFunctionContextType(context: TransformationContext, type: ts.Type): ContextType { + if (type.isTypeParameter()) { + const constraint = type.getConstraint(); + if (constraint) return getFunctionContextType(context, constraint); + } + + const signatures = context.checker.getSignaturesOfType(type, ts.SignatureKind.Call); + if (signatures.length === 0) { + return ContextType.None; + } + + return reduceContextTypes( + signatures.flatMap(s => getSignatureDeclarations(context, s)).map(s => getSignatureContextType(context, s)) + ); +} diff --git a/src/transformation/utils/language-extensions.ts b/src/transformation/utils/language-extensions.ts new file mode 100644 index 000000000..6da38837a --- /dev/null +++ b/src/transformation/utils/language-extensions.ts @@ -0,0 +1,178 @@ +import * as ts from "typescript"; +import { TransformationContext } from "../context"; +import { invalidMethodCallExtensionUse, invalidSpreadInCallExtension } from "./diagnostics"; + +export enum ExtensionKind { + MultiFunction = "MultiFunction", + RangeFunction = "RangeFunction", + VarargConstant = "VarargConstant", + AdditionOperatorType = "Addition", + AdditionOperatorMethodType = "AdditionMethod", + SubtractionOperatorType = "Subtraction", + SubtractionOperatorMethodType = "SubtractionMethod", + MultiplicationOperatorType = "Multiplication", + MultiplicationOperatorMethodType = "MultiplicationMethod", + DivisionOperatorType = "Division", + DivisionOperatorMethodType = "DivisionMethod", + ModuloOperatorType = "Modulo", + ModuloOperatorMethodType = "ModuloMethod", + PowerOperatorType = "Power", + PowerOperatorMethodType = "PowerMethod", + FloorDivisionOperatorType = "FloorDivision", + FloorDivisionOperatorMethodType = "FloorDivisionMethod", + BitwiseAndOperatorType = "BitwiseAnd", + BitwiseAndOperatorMethodType = "BitwiseAndMethod", + BitwiseOrOperatorType = "BitwiseOr", + BitwiseOrOperatorMethodType = "BitwiseOrMethod", + BitwiseExclusiveOrOperatorType = "BitwiseExclusiveOr", + BitwiseExclusiveOrOperatorMethodType = "BitwiseExclusiveOrMethod", + BitwiseLeftShiftOperatorType = "BitwiseLeftShift", + BitwiseLeftShiftOperatorMethodType = "BitwiseLeftShiftMethod", + BitwiseRightShiftOperatorType = "BitwiseRightShift", + BitwiseRightShiftOperatorMethodType = "BitwiseRightShiftMethod", + ConcatOperatorType = "Concat", + ConcatOperatorMethodType = "ConcatMethod", + LessThanOperatorType = "LessThan", + LessThanOperatorMethodType = "LessThanMethod", + GreaterThanOperatorType = "GreaterThan", + GreaterThanOperatorMethodType = "GreaterThanMethod", + NegationOperatorType = "Negation", + NegationOperatorMethodType = "NegationMethod", + BitwiseNotOperatorType = "BitwiseNot", + BitwiseNotOperatorMethodType = "BitwiseNotMethod", + LengthOperatorType = "Length", + LengthOperatorMethodType = "LengthMethod", + TableNewType = "TableNew", + TableDeleteType = "TableDelete", + TableDeleteMethodType = "TableDeleteMethod", + TableGetType = "TableGet", + TableGetMethodType = "TableGetMethod", + TableHasType = "TableHas", + TableHasMethodType = "TableHasMethod", + TableSetType = "TableSet", + TableSetMethodType = "TableSetMethod", + TableAddKeyType = "TableAddKey", + TableAddKeyMethodType = "TableAddKeyMethod", + TableIsEmptyType = "TableIsEmpty", + TableIsEmptyMethodType = "TableIsEmptyMethod", +} + +const extensionValues: Set = new Set(Object.values(ExtensionKind)); + +export function getExtensionKindForType(context: TransformationContext, type: ts.Type): ExtensionKind | undefined { + const value = getPropertyValue(context, type, "__tstlExtension"); + if (value && extensionValues.has(value)) { + return value as ExtensionKind; + } +} + +const excludedTypeFlags: ts.TypeFlags = + ((1 << 18) - 1) | // All flags from Any...Never + ts.TypeFlags.Index | + ts.TypeFlags.NonPrimitive; + +function getPropertyValue(context: TransformationContext, type: ts.Type, propertyName: string): string | undefined { + if (type.flags & excludedTypeFlags) return; + const property = type.getProperty(propertyName); + if (!property) return undefined; + const propertyType = context.checker.getTypeOfSymbolAtLocation(property, context.sourceFile); + if (propertyType.isStringLiteral()) return propertyType.value; +} + +export function getExtensionKindForNode(context: TransformationContext, node: ts.Node): ExtensionKind | undefined { + const originalNode = ts.getOriginalNode(node); + let type = context.checker.getTypeAtLocation(originalNode); + if (ts.isOptionalChain(originalNode)) { + type = context.checker.getNonNullableType(type); + } + return getExtensionKindForType(context, type); +} + +export function getExtensionKindForSymbol( + context: TransformationContext, + symbol: ts.Symbol +): ExtensionKind | undefined { + const type = context.checker.getTypeOfSymbolAtLocation(symbol, context.sourceFile); + return getExtensionKindForType(context, type); +} + +export enum IterableExtensionKind { + Iterable = "Iterable", + Pairs = "Pairs", + PairsKey = "PairsKey", +} + +export function isLuaIterable(context: TransformationContext, type: ts.Type): boolean { + return getPropertyValue(context, type, "__tstlIterable") !== undefined; +} + +export function getIterableExtensionTypeForType( + context: TransformationContext, + type: ts.Type +): IterableExtensionKind | undefined { + const value = getPropertyValue(context, type, "__tstlIterable"); + if (value && value in IterableExtensionKind) { + return value as IterableExtensionKind; + } +} + +export function getIterableExtensionKindForNode( + context: TransformationContext, + node: ts.Node +): IterableExtensionKind | undefined { + const type = context.checker.getTypeAtLocation(node); + return getIterableExtensionTypeForType(context, type); +} + +export const methodExtensionKinds: ReadonlySet = new Set( + Object.values(ExtensionKind).filter(key => key.endsWith("Method")) +); + +export function getNaryCallExtensionArgs( + context: TransformationContext, + node: ts.CallExpression, + kind: ExtensionKind, + numArgs: number +): readonly ts.Expression[] | undefined { + let expressions: readonly ts.Expression[]; + if (node.arguments.some(ts.isSpreadElement)) { + context.diagnostics.push(invalidSpreadInCallExtension(node)); + return undefined; + } + if (methodExtensionKinds.has(kind)) { + if (!(ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))) { + context.diagnostics.push(invalidMethodCallExtensionUse(node)); + return undefined; + } + if (node.arguments.length < numArgs - 1) { + // assumed to be TS error + return undefined; + } + expressions = [node.expression.expression, ...node.arguments]; + } else { + if (node.arguments.length < numArgs) { + // assumed to be TS error + return undefined; + } + expressions = node.arguments; + } + return expressions; +} + +export function getUnaryCallExtensionArg( + context: TransformationContext, + node: ts.CallExpression, + kind: ExtensionKind +): ts.Expression | undefined { + return getNaryCallExtensionArgs(context, node, kind, 1)?.[0]; +} + +export function getBinaryCallExtensionArgs( + context: TransformationContext, + node: ts.CallExpression, + kind: ExtensionKind +): readonly [ts.Expression, ts.Expression] | undefined { + const expressions = getNaryCallExtensionArgs(context, node, kind, 2); + if (expressions === undefined) return undefined; + return [expressions[0], expressions[1]]; +} diff --git a/src/transformation/utils/lua-ast.ts b/src/transformation/utils/lua-ast.ts new file mode 100644 index 000000000..59f6cffd7 --- /dev/null +++ b/src/transformation/utils/lua-ast.ts @@ -0,0 +1,356 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { assert, castArray } from "../../utils"; +import { TransformationContext } from "../context"; +import { createExportedIdentifier, getIdentifierExportScope } from "./export"; +import { peekScope, ScopeType, Scope, addScopeVariableDeclaration } from "./scope"; +import { transformLuaLibFunction } from "./lualib"; +import { LuaLibFeature } from "../../LuaLib"; + +export type OneToManyVisitorResult = T | T[] | undefined; +export function unwrapVisitorResult(result: OneToManyVisitorResult): T[] { + return result === undefined ? [] : castArray(result); +} + +export function createSelfIdentifier(tsOriginal?: ts.Node): lua.Identifier { + return lua.createIdentifier("self", tsOriginal, undefined, "this"); +} + +export function createExportsIdentifier(): lua.Identifier { + return lua.createIdentifier("____exports"); +} + +export function addToNumericExpression(expression: lua.Expression, change: number): lua.Expression { + if (change === 0) return expression; + + const literalValue = getNumberLiteralValue(expression); + if (literalValue !== undefined) { + const newNode = lua.createNumericLiteral(literalValue + change); + lua.setNodePosition(newNode, expression); + return newNode; + } + + if (lua.isBinaryExpression(expression)) { + if ( + lua.isNumericLiteral(expression.right) && + ((expression.operator === lua.SyntaxKind.SubtractionOperator && expression.right.value === change) || + (expression.operator === lua.SyntaxKind.AdditionOperator && expression.right.value === -change)) + ) { + return expression.left; + } + } + + return change > 0 + ? lua.createBinaryExpression(expression, lua.createNumericLiteral(change), lua.SyntaxKind.AdditionOperator) + : lua.createBinaryExpression(expression, lua.createNumericLiteral(-change), lua.SyntaxKind.SubtractionOperator); +} + +export function getNumberLiteralValue(expression?: lua.Expression) { + if (!expression) return undefined; + + if (lua.isNumericLiteral(expression)) return expression.value; + + if ( + lua.isUnaryExpression(expression) && + expression.operator === lua.SyntaxKind.NegationOperator && + lua.isNumericLiteral(expression.operand) + ) { + return -expression.operand.value; + } + + return undefined; +} + +export function createUnpackCall( + context: TransformationContext, + expression: lua.Expression, + tsOriginal?: ts.Node +): lua.Expression { + if (context.luaTarget === LuaTarget.Universal) { + return transformLuaLibFunction(context, LuaLibFeature.Unpack, tsOriginal, expression); + } + + 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], 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; +} + +export function wrapInTable(...expressions: lua.Expression[]): lua.TableExpression { + const fields = expressions.map(e => lua.createTableFieldExpression(e)); + return lua.createTableExpression(fields); +} + +export function wrapInToStringForConcat(expression: lua.Expression): lua.Expression { + if ( + lua.isStringLiteral(expression) || + lua.isNumericLiteral(expression) || + (lua.isBinaryExpression(expression) && expression.operator === lua.SyntaxKind.ConcatOperator) + ) { + return expression; + } + + return lua.createCallExpression(lua.createIdentifier("tostring"), [expression]); +} + +export function createHoistableVariableDeclarationStatement( + context: TransformationContext, + identifier: lua.Identifier, + initializer?: lua.Expression, + tsOriginal?: ts.Node +): lua.AssignmentStatement | lua.VariableDeclarationStatement { + const declaration = lua.createVariableDeclarationStatement(identifier, initializer, tsOriginal); + if (identifier.symbolId !== undefined) { + const scope = peekScope(context); + assert(scope.type !== ScopeType.Switch); + addScopeVariableDeclaration(scope, declaration); + } + + return declaration; +} + +function hasMultipleReferences(scope: Scope, identifiers: lua.Identifier | lua.Identifier[]) { + const scopeSymbols = scope.referencedSymbols; + if (!scopeSymbols) { + return false; + } + + const referenceLists = castArray(identifiers).map(i => i.symbolId && scopeSymbols.get(i.symbolId)); + + return referenceLists.some(symbolRefs => symbolRefs && symbolRefs.length > 1); +} + +export function createLocalOrExportedOrGlobalDeclaration( + context: TransformationContext, + lhs: lua.Identifier | lua.Identifier[], + rhs?: lua.Expression | lua.Expression[], + tsOriginal?: ts.Node, + overrideExportScope?: ts.SourceFile | ts.ModuleDeclaration +): lua.Statement[] { + let declaration: lua.VariableDeclarationStatement | undefined; + let assignment: lua.AssignmentStatement | undefined; + + const noImplicitGlobalVariables = context.options.noImplicitGlobalVariables === true; + + const isFunctionDeclaration = tsOriginal !== undefined && ts.isFunctionDeclaration(tsOriginal); + + const identifiers = castArray(lhs); + if (identifiers.length === 0) { + return []; + } + + const exportScope = overrideExportScope ?? getIdentifierExportScope(context, identifiers[0]); + if (exportScope) { + // exported + if (!rhs) { + return []; + } else { + assignment = lua.createAssignmentStatement( + identifiers.map(identifier => createExportedIdentifier(context, identifier, exportScope)), + rhs, + tsOriginal + ); + } + } else { + const scope = peekScope(context); + const isTopLevelVariable = scope.type === ScopeType.File; + + if (context.isModule || !isTopLevelVariable || noImplicitGlobalVariables) { + const isLuaFunctionExpression = rhs && !Array.isArray(rhs) && lua.isFunctionExpression(rhs); + const isSafeRecursiveFunctionDeclaration = isFunctionDeclaration && isLuaFunctionExpression; + if (!isSafeRecursiveFunctionDeclaration && hasMultipleReferences(scope, lhs)) { + // Split declaration and assignment of identifiers that reference themselves in their declaration. + // Put declaration above preceding statements in case the identifier is referenced in those. + const precedingDeclaration = lua.createVariableDeclarationStatement(lhs, undefined, tsOriginal); + context.prependPrecedingStatements(precedingDeclaration); + if (rhs) { + assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); + } + + // Remember local variable declarations for hoisting later + addScopeVariableDeclaration(scope, precedingDeclaration); + } else { + declaration = lua.createVariableDeclarationStatement(lhs, rhs, tsOriginal); + + if (!isFunctionDeclaration) { + // Remember local variable declarations for hoisting later + addScopeVariableDeclaration(scope, declaration); + } + } + } else if (rhs) { + // global + assignment = lua.createAssignmentStatement(lhs, rhs, tsOriginal); + } else { + return []; + } + } + + if (isFunctionDeclaration) { + // Remember function definitions for hoisting later + const functionSymbolId = (lhs as lua.Identifier).symbolId; + const scope = peekScope(context); + if (functionSymbolId && scope.functionDefinitions) { + const definitions = scope.functionDefinitions.get(functionSymbolId); + if (definitions) { + definitions.definition = declaration ?? assignment; + } + } + } + + setJSDocComments(context, tsOriginal, declaration, assignment); + + if (declaration && assignment) { + return [declaration, assignment]; + } else if (declaration) { + return [declaration]; + } else if (assignment) { + return [assignment]; + } else { + return []; + } +} + +/** + * Apply JSDoc comments to the newly-created Lua statement, if present. + * https://stackoverflow.com/questions/47429792/is-it-possible-to-get-comments-as-nodes-in-the-ast-using-the-typescript-compiler + */ +function setJSDocComments( + context: TransformationContext, + tsOriginal: ts.Node | undefined, + declaration: lua.VariableDeclarationStatement | undefined, + assignment: lua.AssignmentStatement | undefined +) { + // Respect the vanilla TypeScript option of "removeComments": + // https://www.typescriptlang.org/tsconfig#removeComments + if (context.options.removeComments) { + return; + } + + const docCommentArray = getJSDocCommentFromTSNode(context, tsOriginal); + if (docCommentArray === undefined) { + return; + } + + if (declaration && assignment) { + declaration.leadingComments = docCommentArray; + } else if (declaration) { + declaration.leadingComments = docCommentArray; + } else if (assignment) { + assignment.leadingComments = docCommentArray; + } +} + +function getJSDocCommentFromTSNode( + context: TransformationContext, + tsOriginal: ts.Node | undefined +): string[] | undefined { + if (tsOriginal === undefined) { + return undefined; + } + + // The "name" property is only on a subset of node types; we want to be permissive and get the + // comments from as many nodes as possible. + const node = tsOriginal as any; + if (node.name === undefined) { + return undefined; + } + + const symbol = context.checker.getSymbolAtLocation(node.name); + if (symbol === undefined) { + return undefined; + } + + // The TypeScript compiler separates JSDoc comments into the "documentation comment" and the + // "tags". The former is conventionally at the top of the comment, and the bottom is + // conventionally at the bottom. We need to get both from the TypeScript API and then combine + // them into one block of text. + const docCommentArray = symbol.getDocumentationComment(context.checker); + const docCommentText = ts.displayPartsToString(docCommentArray).trim(); + + const jsDocTagInfoArray = symbol.getJsDocTags(context.checker); + const jsDocTagsTextLines = jsDocTagInfoArray.map(jsDocTagInfo => { + let text = "@" + jsDocTagInfo.name; + if (jsDocTagInfo.text !== undefined) { + const tagDescriptionTextArray = jsDocTagInfo.text + .filter(symbolDisplayPart => symbolDisplayPart.text.trim() !== "") + .map(symbolDisplayPart => symbolDisplayPart.text.trim()); + const tagDescriptionText = tagDescriptionTextArray.join(" "); + text += " " + tagDescriptionText; + } + return text; + }); + const jsDocTagsText = jsDocTagsTextLines.join("\n"); + + const combined = (docCommentText + "\n\n" + jsDocTagsText).trim(); + if (combined === "") { + return undefined; + } + + // By default, TSTL will display comments immediately next to the "--" characters. We can make + // the comments look better if we separate them by a space (similar to what Prettier does in + // JavaScript/TypeScript). + const linesWithoutSpace = combined.split("\n"); + const lines = linesWithoutSpace.map(line => ` ${line}`); + + // We want to JSDoc comments to map on to LDoc comments: + // https://stevedonovan.github.io/ldoc/manual/doc.md.html + // LDoc comments require that the first line starts with three hyphens. + // Thus, need to add a hyphen to the first line. + if (lines.length > 0) { + const firstLine = lines[0]; + if (firstLine.startsWith(" @")) { + lines.unshift("-"); + } else { + lines.shift(); + lines.unshift("-" + firstLine); + } + + return lines; + } +} + +export const createNaN = (tsOriginal?: ts.Node) => + lua.createBinaryExpression( + lua.createNumericLiteral(0), + lua.createNumericLiteral(0), + lua.SyntaxKind.DivisionOperator, + tsOriginal + ); diff --git a/src/transformation/utils/lualib.ts b/src/transformation/utils/lualib.ts new file mode 100644 index 000000000..ee828d1b9 --- /dev/null +++ b/src/transformation/utils/lualib.ts @@ -0,0 +1,21 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { LuaLibFeature } from "../../LuaLib"; +import { TransformationContext } from "../context"; + +export { LuaLibFeature }; + +export function importLuaLibFeature(context: TransformationContext, feature: LuaLibFeature): void { + context.usedLuaLibFeatures.add(feature); +} + +export function transformLuaLibFunction( + context: TransformationContext, + feature: LuaLibFeature, + tsParent?: ts.Node, + ...params: lua.Expression[] +): lua.CallExpression { + importLuaLibFeature(context, feature); + const functionIdentifier = lua.createIdentifier(`__TS__${feature}`); + return lua.createCallExpression(functionIdentifier, params, tsParent); +} diff --git a/src/transformation/utils/preceding-statements.ts b/src/transformation/utils/preceding-statements.ts new file mode 100644 index 000000000..16cefac12 --- /dev/null +++ b/src/transformation/utils/preceding-statements.ts @@ -0,0 +1,18 @@ +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; + +export interface WithPrecedingStatements< + T extends lua.Statement | lua.Statement[] | lua.Expression | lua.Expression[] +> { + precedingStatements: lua.Statement[]; + result: T; +} + +export function transformInPrecedingStatementScope< + TReturn extends lua.Statement | lua.Statement[] | lua.Expression | lua.Expression[] +>(context: TransformationContext, transformer: () => TReturn): WithPrecedingStatements { + context.pushPrecedingStatements(); + const statementOrStatements = transformer(); + const precedingStatements = context.popPrecedingStatements(); + return { precedingStatements, result: statementOrStatements }; +} diff --git a/src/transformation/utils/safe-names.ts b/src/transformation/utils/safe-names.ts new file mode 100644 index 000000000..0772d71a8 --- /dev/null +++ b/src/transformation/utils/safe-names.ts @@ -0,0 +1,113 @@ +import * as ts from "typescript"; +import { CompilerOptions, LuaTarget } from "../.."; +import { TransformationContext } from "../context"; +import { invalidAmbientIdentifierName } from "./diagnostics"; +import { isSymbolExported } from "./export"; +import { isAmbientNode } from "./typescript"; + +export const shouldAllowUnicode = (options: CompilerOptions) => options.luaTarget === LuaTarget.LuaJIT; + +export const isValidLuaIdentifier = (name: string, options: CompilerOptions) => + !luaKeywords.has(name) && + (shouldAllowUnicode(options) + ? /^[a-zA-Z_\u007F-\uFFFD][a-zA-Z0-9_\u007F-\uFFFD]*$/ + : /^[a-zA-Z_][a-zA-Z0-9_]*$/ + ).test(name); + +export const luaKeywords: ReadonlySet = new Set([ + "and", + "bit", + "bit32", + "break", + "do", + "else", + "elseif", + "end", + "false", + "for", + "function", + "goto", + "if", + "in", + "local", + "nil", + "not", + "or", + "repeat", + "return", + "then", + "true", + "until", + "while", +]); + +const luaBuiltins: ReadonlySet = new Set([ + "_G", + "assert", + "coroutine", + "debug", + "error", + "ipairs", + "math", + "pairs", + "pcall", + "print", + "rawget", + "repeat", + "require", + "self", + "string", + "table", + "tostring", + "type", + "unpack", +]); + +export const isUnsafeName = (name: string, options: CompilerOptions) => + !isValidLuaIdentifier(name, options) || luaBuiltins.has(name); + +function checkName(context: TransformationContext, name: string, node: ts.Node): boolean { + const isInvalid = !isValidLuaIdentifier(name, context.options); + + if (isInvalid) { + // Empty identifier is a TypeScript error + if (name !== "") { + context.diagnostics.push(invalidAmbientIdentifierName(node, name)); + } + } + + return isInvalid; +} + +export function hasUnsafeSymbolName( + context: TransformationContext, + symbol: ts.Symbol, + tsOriginal: ts.Identifier +): boolean { + const isAmbient = symbol.declarations?.some(d => isAmbientNode(d)) ?? false; + + // Catch ambient declarations of identifiers with bad names + if (isAmbient && checkName(context, symbol.name, tsOriginal)) { + return true; + } + + // only unsafe when non-ambient and not exported + return isUnsafeName(symbol.name, context.options) && !isAmbient && !isSymbolExported(context, symbol); +} + +export function hasUnsafeIdentifierName( + context: TransformationContext, + identifier: ts.Identifier, + symbol: ts.Symbol | undefined +): boolean { + if (symbol) { + return hasUnsafeSymbolName(context, symbol, identifier); + } + + return checkName(context, identifier.text, identifier); +} + +const fixInvalidLuaIdentifier = (name: string) => + name.replace(/[^a-zA-Z0-9_]/g, c => `_${c.charCodeAt(0).toString(16).toUpperCase()}`); + +export const createSafeName = (name: string) => "____" + fixInvalidLuaIdentifier(name); diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts new file mode 100644 index 000000000..fa66ee3e2 --- /dev/null +++ b/src/transformation/utils/scope.ts @@ -0,0 +1,287 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { assert, getOrUpdate, isNonNull } from "../../utils"; +import { TransformationContext } from "../context"; +import { getSymbolInfo } from "./symbols"; +import { findFirstNodeAbove, getFirstDeclarationInFile } from "./typescript"; + +export enum ScopeType { + File = 1 << 0, + Function = 1 << 1, + Switch = 1 << 2, + Loop = 1 << 3, + Conditional = 1 << 4, + Block = 1 << 5, + Try = 1 << 6, + Catch = 1 << 7, + LoopInitializer = 1 << 8, +} + +interface FunctionDefinitionInfo { + referencedSymbols: Map; + definition?: lua.VariableDeclarationStatement | lua.AssignmentStatement; +} + +export enum LoopContinued { + WithGoto, + WithRepeatBreak, + WithContinue, +} + +export interface Scope { + type: ScopeType; + id: number; + node: ts.Node; + referencedSymbols?: Map; + variableDeclarations?: lua.VariableDeclarationStatement[]; + functionDefinitions?: Map; + importStatements?: lua.Statement[]; + loopContinued?: LoopContinued; + functionReturned?: boolean; +} + +export interface HoistingResult { + statements: lua.Statement[]; + hoistedStatements: lua.Statement[]; + hoistedIdentifiers: lua.Identifier[]; +} + +export function* walkScopesUp(context: TransformationContext): IterableIterator { + const scopeStack = context.scopeStack; + for (let i = scopeStack.length - 1; i >= 0; --i) { + const scope = scopeStack[i]; + yield scope; + } +} + +export function markSymbolAsReferencedInCurrentScopes( + context: TransformationContext, + symbolId: lua.SymbolId, + identifier: ts.Identifier +): void { + for (const scope of context.scopeStack) { + scope.referencedSymbols ??= new Map(); + + const references = getOrUpdate(scope.referencedSymbols, symbolId, () => []); + references.push(identifier); + } +} + +export function peekScope(context: TransformationContext): Scope { + const scopeStack = context.scopeStack; + const scope = scopeStack[scopeStack.length - 1]; + assert(scope); + + return scope; +} + +export function findScope(context: TransformationContext, scopeTypes: ScopeType): Scope | undefined { + for (let i = context.scopeStack.length - 1; i >= 0; --i) { + const scope = context.scopeStack[i]; + if (scopeTypes & scope.type) { + return scope; + } + } +} + +export function addScopeVariableDeclaration(scope: Scope, declaration: lua.VariableDeclarationStatement) { + scope.variableDeclarations ??= []; + + scope.variableDeclarations.push(declaration); +} + +function isHoistableFunctionDeclaredInScope(symbol: ts.Symbol, scopeNode: ts.Node) { + return symbol?.declarations?.some( + d => ts.isFunctionDeclaration(d) && findFirstNodeAbove(d, (n): n is ts.Node => n === scopeNode) + ); +} + +// 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) { + return false; + } + for (const [symbolId, nodes] of scope.referencedSymbols) { + const type = context.checker.getTypeAtLocation(nodes[0]); + if ( + !scope.functionDefinitions?.has(symbolId) && + type.getCallSignatures().length > 0 && + isHoistableFunctionDeclaredInScope(type.symbol, scope.node) + ) { + return true; + } + } + return false; +} + +export function hasReferencedSymbol(context: TransformationContext, scope: Scope, symbol: ts.Symbol) { + if (!scope.referencedSymbols) { + return; + } + for (const nodes of scope.referencedSymbols.values()) { + if (nodes.some(node => context.checker.getSymbolAtLocation(node) === symbol)) { + return true; + } + } + return false; +} + +export function separateHoistedStatements(context: TransformationContext, statements: lua.Statement[]): HoistingResult { + const scope = peekScope(context); + const allHoistedStatments: lua.Statement[] = []; + const allHoistedIdentifiers: lua.Identifier[] = []; + + let { unhoistedStatements, hoistedStatements, hoistedIdentifiers } = hoistFunctionDefinitions( + context, + scope, + statements + ); + allHoistedStatments.push(...hoistedStatements); + allHoistedIdentifiers.push(...hoistedIdentifiers); + + ({ unhoistedStatements, hoistedIdentifiers } = hoistVariableDeclarations(context, scope, unhoistedStatements)); + allHoistedIdentifiers.push(...hoistedIdentifiers); + + ({ unhoistedStatements, hoistedStatements } = hoistImportStatements(scope, unhoistedStatements)); + allHoistedStatments.unshift(...hoistedStatements); + + return { + statements: unhoistedStatements, + hoistedStatements: allHoistedStatments, + hoistedIdentifiers: allHoistedIdentifiers, + }; +} + +export function performHoisting(context: TransformationContext, statements: lua.Statement[]): lua.Statement[] { + const result = separateHoistedStatements(context, statements); + const modifiedStatements = [...result.hoistedStatements, ...result.statements]; + if (result.hoistedIdentifiers.length > 0) { + modifiedStatements.unshift(lua.createVariableDeclarationStatement(result.hoistedIdentifiers)); + } + return modifiedStatements; +} + +function shouldHoistSymbol(context: TransformationContext, symbolId: lua.SymbolId, scope: Scope): boolean { + // Always hoist in top-level of switch statements + if (scope.type === ScopeType.Switch) { + return true; + } + + const symbolInfo = getSymbolInfo(context, symbolId); + if (!symbolInfo) { + return false; + } + + const declaration = getFirstDeclarationInFile(symbolInfo.symbol, context.sourceFile); + if (!declaration) { + return false; + } + + if (symbolInfo.firstSeenAtPos < declaration.pos) { + return true; + } + + if (scope.functionDefinitions) { + for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { + assert(functionDefinition.definition); + + const { line, column } = lua.getOriginalPos(functionDefinition.definition); + if (line !== undefined && column !== undefined) { + const definitionPos = ts.getPositionOfLineAndCharacter(context.sourceFile, line, column); + if ( + functionSymbolId !== symbolId && // Don't recurse into self + declaration.pos < definitionPos && // Ignore functions before symbol declaration + functionDefinition.referencedSymbols.has(symbolId) && + shouldHoistSymbol(context, functionSymbolId, scope) + ) { + return true; + } + } + } + } + + return false; +} + +function hoistVariableDeclarations( + context: TransformationContext, + scope: Scope, + statements: lua.Statement[] +): { unhoistedStatements: lua.Statement[]; hoistedIdentifiers: lua.Identifier[] } { + if (!scope.variableDeclarations) { + return { unhoistedStatements: statements, hoistedIdentifiers: [] }; + } + + const unhoistedStatements = [...statements]; + const hoistedIdentifiers: lua.Identifier[] = []; + for (const declaration of scope.variableDeclarations) { + const symbols = declaration.left.map(i => i.symbolId).filter(isNonNull); + if (symbols.some(s => shouldHoistSymbol(context, s, scope))) { + const index = unhoistedStatements.indexOf(declaration); + if (index < 0) { + continue; // statements array may not contain all statements in the scope (switch-case) + } + + if (declaration.right) { + const assignment = lua.createAssignmentStatement(declaration.left, declaration.right); + lua.setNodePosition(assignment, declaration); // Preserve position info for sourcemap + unhoistedStatements.splice(index, 1, assignment); + } else { + unhoistedStatements.splice(index, 1); + } + + hoistedIdentifiers.push(...declaration.left); + } + } + + return { unhoistedStatements, hoistedIdentifiers }; +} + +function hoistFunctionDefinitions( + context: TransformationContext, + scope: Scope, + statements: lua.Statement[] +): { unhoistedStatements: lua.Statement[]; hoistedStatements: lua.Statement[]; hoistedIdentifiers: lua.Identifier[] } { + if (!scope.functionDefinitions) { + return { unhoistedStatements: statements, hoistedStatements: [], hoistedIdentifiers: [] }; + } + + const unhoistedStatements = [...statements]; + const hoistedStatements: lua.Statement[] = []; + const hoistedIdentifiers: lua.Identifier[] = []; + for (const [functionSymbolId, functionDefinition] of scope.functionDefinitions) { + assert(functionDefinition.definition); + + if (shouldHoistSymbol(context, functionSymbolId, scope)) { + const index = unhoistedStatements.indexOf(functionDefinition.definition); + if (index < 0) { + continue; // statements array may not contain all statements in the scope (switch-case) + } + unhoistedStatements.splice(index, 1); + + if (lua.isVariableDeclarationStatement(functionDefinition.definition)) { + // Separate function definition and variable declaration + assert(functionDefinition.definition.right); + hoistedIdentifiers.push(...functionDefinition.definition.left); + hoistedStatements.push( + lua.createAssignmentStatement( + functionDefinition.definition.left, + functionDefinition.definition.right + ) + ); + } else { + hoistedStatements.push(functionDefinition.definition); + } + } + } + + return { unhoistedStatements, hoistedStatements, hoistedIdentifiers }; +} + +function hoistImportStatements( + scope: Scope, + statements: lua.Statement[] +): { unhoistedStatements: lua.Statement[]; hoistedStatements: lua.Statement[] } { + return { unhoistedStatements: statements, hoistedStatements: scope.importStatements ?? [] }; +} diff --git a/src/transformation/utils/symbols.ts b/src/transformation/utils/symbols.ts new file mode 100644 index 000000000..d79b68da8 --- /dev/null +++ b/src/transformation/utils/symbols.ts @@ -0,0 +1,51 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext } from "../context"; +import { isOptimizedVarArgSpread } from "../visitors/spread"; +import { markSymbolAsReferencedInCurrentScopes } from "./scope"; + +export interface SymbolInfo { + symbol: ts.Symbol; + firstSeenAtPos: number; +} + +export function getSymbolInfo(context: TransformationContext, symbolId: lua.SymbolId): SymbolInfo | undefined { + return context.symbolInfoMap.get(symbolId); +} + +export function getSymbolIdOfSymbol(context: TransformationContext, symbol: ts.Symbol): lua.SymbolId | undefined { + return context.symbolIdMaps.get(symbol); +} + +export function trackSymbolReference( + context: TransformationContext, + symbol: ts.Symbol, + identifier: ts.Identifier +): lua.SymbolId | undefined { + // Track first time symbols are seen + let symbolId = context.symbolIdMaps.get(symbol); + if (symbolId === undefined) { + symbolId = context.nextSymbolId(); + + context.symbolIdMaps.set(symbol, symbolId); + context.symbolInfoMap.set(symbolId, { symbol, firstSeenAtPos: identifier.pos }); + } + + // If isOptimizedVarArgSpread returns true, the identifier will not appear in the resulting Lua. + // Only the optimized ellipses (...) will be used. + if (!isOptimizedVarArgSpread(context, symbol, identifier)) { + markSymbolAsReferencedInCurrentScopes(context, symbolId, identifier); + } + + return symbolId; +} + +export function getIdentifierSymbolId( + context: TransformationContext, + identifier: ts.Identifier, + symbol: ts.Symbol | undefined +): lua.SymbolId | undefined { + if (symbol) { + return trackSymbolReference(context, symbol, identifier); + } +} diff --git a/src/transformation/utils/typescript/index.ts b/src/transformation/utils/typescript/index.ts new file mode 100644 index 000000000..e5984b08f --- /dev/null +++ b/src/transformation/utils/typescript/index.ts @@ -0,0 +1,123 @@ +import * as ts from "typescript"; +import { TransformationContext } from "../../context"; + +export * from "./nodes"; +export * from "./types"; + +// TODO: Move to separate files? + +export function hasExportEquals(sourceFile: ts.SourceFile): boolean { + return sourceFile.statements.some(node => ts.isExportAssignment(node) && node.isExportEquals); +} + +/** + * 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; + while (current.parent) { + if (callback(current.parent)) { + return current.parent; + } else { + current = current.parent; + } + } +} + +export function findFirstNonOuterParent(node: ts.Node): ts.Node { + let current = ts.getOriginalNode(node).parent; + while (ts.isOuterExpression(current)) { + current = ts.getOriginalNode(current).parent; + } + return current; +} + +export function expressionResultIsUsed(node: ts.Expression): boolean { + return !ts.isExpressionStatement(findFirstNonOuterParent(node)); +} + +export function getFirstDeclarationInFile(symbol: ts.Symbol, sourceFile: ts.SourceFile): ts.Declaration | undefined { + const originalSourceFile = ts.getParseTreeNode(sourceFile) ?? sourceFile; + const declarations = (symbol.getDeclarations() ?? []).filter(d => d.getSourceFile() === originalSourceFile); + + return declarations.length > 0 ? declarations.reduce((p, c) => (p.pos < c.pos ? p : c)) : undefined; +} + +export function isStandardLibraryDeclaration(context: TransformationContext, declaration: ts.Declaration): boolean { + const parseTreeNode = ts.getParseTreeNode(declaration) ?? declaration; + const sourceFile = parseTreeNode.getSourceFile(); + if (!sourceFile) { + return false; + } + + return context.program.isSourceFileDefaultLibrary(sourceFile); +} + +export function isStandardLibraryType( + context: TransformationContext, + type: ts.Type, + name: string | undefined +): boolean { + const symbol = type.getSymbol(); + if (!symbol || (name ? symbol.name !== name : symbol.name === "__type")) { + return false; + } + + // Assume to be lib function if no valueDeclaration exists + const declaration = symbol.valueDeclaration; + if (!declaration) { + return true; + } + + return isStandardLibraryDeclaration(context, declaration); +} + +export function hasStandardLibrarySignature( + context: TransformationContext, + callExpression: ts.CallExpression +): boolean { + const signature = context.checker.getResolvedSignature(callExpression); + return signature?.declaration ? isStandardLibraryDeclaration(context, signature.declaration) : false; +} + +export function inferAssignedType(context: TransformationContext, expression: ts.Expression): ts.Type { + return context.checker.getContextualType(expression) ?? context.checker.getTypeAtLocation(expression); +} + +export function getAllCallSignatures(type: ts.Type): readonly ts.Signature[] { + return type.isUnion() ? type.types.flatMap(getAllCallSignatures) : type.getCallSignatures(); +} + +// Returns true for expressions that may have effects when evaluated +export function isExpressionWithEvaluationEffect(node: ts.Expression): boolean { + return !(ts.isLiteralExpression(node) || ts.isIdentifier(node) || node.kind === ts.SyntaxKind.ThisKeyword); +} + +export function getFunctionTypeForCall(context: TransformationContext, node: ts.CallExpression) { + const signature = context.checker.getResolvedSignature(node); + if (!signature?.declaration) { + return; + } + const typeDeclaration = findFirstNodeAbove(signature.declaration, ts.isTypeAliasDeclaration); + if (!typeDeclaration) { + return; + } + return context.checker.getTypeFromTypeNode(typeDeclaration.type); +} + +export function isConstIdentifier(context: TransformationContext, node: ts.Node) { + let identifier = node; + if (ts.isComputedPropertyName(identifier)) { + identifier = identifier.expression; + } + if (!ts.isIdentifier(identifier)) { + return false; + } + const symbol = context.checker.getSymbolAtLocation(identifier); + if (!symbol?.declarations) { + return false; + } + return symbol.declarations.some( + d => ts.isVariableDeclarationList(d.parent) && (d.parent.flags & ts.NodeFlags.Const) !== 0 + ); +} diff --git a/src/transformation/utils/typescript/nodes.ts b/src/transformation/utils/typescript/nodes.ts new file mode 100644 index 000000000..09e2791c7 --- /dev/null +++ b/src/transformation/utils/typescript/nodes.ts @@ -0,0 +1,63 @@ +import * as ts from "typescript"; +import { findFirstNodeAbove } from "."; +import { TransformationContext } from "../../context"; + +export function isAssignmentPattern(node: ts.Node): node is ts.AssignmentPattern { + return ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node); +} + +export function isDestructuringAssignment(node: ts.Node): node is ts.DestructuringAssignment { + return ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isAssignmentPattern(node.left) + ); +} + +export function isAmbientNode(node: ts.Declaration): boolean { + return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Ambient) !== 0; +} + +export function isInDestructingAssignment(node: ts.Node): boolean { + return ( + node.parent && + ((ts.isVariableDeclaration(node.parent) && ts.isArrayBindingPattern(node.parent.name)) || + (ts.isBinaryExpression(node.parent) && ts.isArrayLiteralExpression(node.parent.left))) + ); +} + +export function isInAsyncFunction(node: ts.Node): boolean { + // Check if node is in function declaration with `async` + const declaration = findFirstNodeAbove(node, ts.isFunctionLike); + if (!declaration) { + return false; + } + + if (ts.canHaveModifiers(declaration)) { + return ts.getModifiers(declaration)?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false; + } else { + return false; + } +} + +export function isInGeneratorFunction(node: ts.Node): boolean { + // Check if node is in function declaration with `async` + const declaration = findFirstNodeAbove(node, ts.isFunctionDeclaration); + if (!declaration) { + return false; + } + + return declaration.asteriskToken !== undefined; +} + +/** + * Quite hacky, avoid unless absolutely necessary! + */ +export function getSymbolOfNode(context: TransformationContext, node: ts.Node): ts.Symbol | undefined { + return (node as any).symbol ?? context.checker.getSymbolAtLocation(node); +} + +export function isFirstDeclaration(context: TransformationContext, node: ts.Node) { + const symbol = getSymbolOfNode(context, node); + return symbol ? symbol.valueDeclaration === node : true; +} diff --git a/src/transformation/utils/typescript/types.ts b/src/transformation/utils/typescript/types.ts new file mode 100644 index 000000000..4a356b972 --- /dev/null +++ b/src/transformation/utils/typescript/types.ts @@ -0,0 +1,156 @@ +import * as ts from "typescript"; +import { TransformationContext } from "../../context"; + +export function typeAlwaysHasSomeOfFlags(context: TransformationContext, type: ts.Type, flags: ts.TypeFlags): boolean { + const baseConstraint = context.checker.getBaseConstraintOfType(type); + if (baseConstraint) { + type = baseConstraint; + } + + if (type.flags & flags) { + return true; + } + + if (type.isUnion()) { + return type.types.every(t => typeAlwaysHasSomeOfFlags(context, t, flags)); + } + + if (type.isIntersection()) { + return type.types.some(t => typeAlwaysHasSomeOfFlags(context, t, flags)); + } + + return false; +} + +export function typeCanHaveSomeOfFlags(context: TransformationContext, type: ts.Type, flags: ts.TypeFlags): boolean { + const baseConstraint = context.checker.getBaseConstraintOfType(type); + if (!baseConstraint) { + // type parameter with no constraint can be anything, assume it might satisfy predicate + if (type.isTypeParameter()) return true; + } else { + type = baseConstraint; + } + + if (type.flags & flags) { + return true; + } + + if (type.isUnion()) { + return type.types.some(t => typeCanHaveSomeOfFlags(context, t, flags)); + } + + if (type.isIntersection()) { + return type.types.some(t => typeCanHaveSomeOfFlags(context, t, flags)); + } + + return false; +} + +export function isStringType(context: TransformationContext, type: ts.Type): boolean { + return typeAlwaysHasSomeOfFlags(context, type, ts.TypeFlags.StringLike); +} + +export function isNumberType(context: TransformationContext, type: ts.Type): boolean { + return typeAlwaysHasSomeOfFlags(context, type, ts.TypeFlags.NumberLike); +} + +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) { + return isExplicitArrayType(context, baseConstraint); + } + } + + return false; +} + +function isAlwaysExplicitArrayType(context: TransformationContext, type: ts.Type): boolean { + if (context.checker.isArrayType(type) || context.checker.isTupleType(type)) return true; + if (type.symbol) { + const baseConstraint = context.checker.getBaseConstraintOfType(type); + if (baseConstraint && baseConstraint !== type) { + return isAlwaysExplicitArrayType(context, baseConstraint); + } + } + + if (type.isUnionOrIntersection()) { + return type.types.every(t => isAlwaysExplicitArrayType(context, t)); + } + + return false; +} + +/** + * Iterate over a type and its bases until the callback returns true. + */ +export function forTypeOrAnySupertype( + context: TransformationContext, + type: ts.Type, + predicate: (type: ts.Type) => boolean +): boolean { + if (predicate(type)) { + return true; + } + + if (!type.isClassOrInterface() && type.symbol) { + type = context.checker.getDeclaredTypeOfSymbol(type.symbol); + } + + const baseTypes = type.getBaseTypes(); + if (!baseTypes) return false; + return baseTypes.some(superType => forTypeOrAnySupertype(context, superType, predicate)); +} + +export function isArrayType(context: TransformationContext, type: ts.Type): boolean { + return forTypeOrAnySupertype(context, type, t => isExplicitArrayType(context, t)); +} + +export function isAlwaysArrayType(context: TransformationContext, type: ts.Type): boolean { + return forTypeOrAnySupertype(context, type, t => isAlwaysExplicitArrayType(context, t)); +} + +export function isFunctionType(type: ts.Type): boolean { + return type.getCallSignatures().length > 0; +} + +export function canBeFalsy(context: TransformationContext, type: ts.Type): boolean { + const strictNullChecks = context.options.strict === true || context.options.strictNullChecks === true; + if (!strictNullChecks && !type.isLiteral()) return true; + const falsyFlags = + ts.TypeFlags.Boolean | + ts.TypeFlags.BooleanLiteral | + ts.TypeFlags.Never | + ts.TypeFlags.Void | + ts.TypeFlags.Unknown | + ts.TypeFlags.Any | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null; + return typeCanHaveSomeOfFlags(context, type, falsyFlags); +} + +export function canBeFalsyWhenNotNull(context: TransformationContext, type: ts.Type): boolean { + const falsyFlags = + ts.TypeFlags.Boolean | + ts.TypeFlags.BooleanLiteral | + ts.TypeFlags.Never | + ts.TypeFlags.Void | + ts.TypeFlags.Unknown | + ts.TypeFlags.Any; + return typeCanHaveSomeOfFlags(context, type, falsyFlags); +} diff --git a/src/transformation/visitors/access.ts b/src/transformation/visitors/access.ts new file mode 100644 index 000000000..7c96bdcd8 --- /dev/null +++ b/src/transformation/visitors/access.ts @@ -0,0 +1,223 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { transformBuiltinPropertyAccessExpression } from "../builtins"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { AnnotationKind, getTypeAnnotations } from "../utils/annotations"; +import { + invalidCallExtensionUse, + invalidMultiReturnAccess, + unsupportedOptionalCompileMembersOnly, +} from "../utils/diagnostics"; +import { getExtensionKindForNode } from "../utils/language-extensions"; +import { addToNumericExpression, createExportsIdentifier } from "../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { isArrayType, isNumberType, isStringType } from "../utils/typescript"; +import { tryGetConstEnumValue } from "./enum"; +import { transformOrderedExpressions } from "./expression-list"; +import { callExtensions } from "./language-extensions/call-extension"; +import { isMultiReturnCall, returnsMultiType } from "./language-extensions/multi"; +import { + transformOptionalChainWithCapture, + ExpressionWithThisValue, + isOptionalContinuation, + captureThisValue, +} from "./optional-chaining"; +import { SyntaxKind } from "typescript"; +import { getCustomNameFromSymbol } from "./identifier"; +import { getSymbolExportScope, isSymbolExported } from "../utils/export"; + +function addOneToArrayAccessArgument( + context: TransformationContext, + node: ts.ElementAccessExpression, + index: lua.Expression +): lua.Expression { + const type = context.checker.getTypeAtLocation(node.expression); + const argumentType = context.checker.getTypeAtLocation(node.argumentExpression); + if (isArrayType(context, type) && isNumberType(context, argumentType)) { + return addToNumericExpression(index, 1); + } + return index; +} + +export function transformElementAccessArgument( + context: TransformationContext, + node: ts.ElementAccessExpression +): lua.Expression { + const index = context.transformExpression(node.argumentExpression); + return addOneToArrayAccessArgument(context, node, index); +} + +export const transformElementAccessExpression: FunctionVisitor = (node, context) => + transformElementAccessExpressionWithCapture(context, node, undefined).expression; +export function transformElementAccessExpressionWithCapture( + context: TransformationContext, + node: ts.ElementAccessExpression, + thisValueCapture: lua.Identifier | undefined +): ExpressionWithThisValue { + const constEnumValue = tryGetConstEnumValue(context, node); + if (constEnumValue) { + return { expression: constEnumValue }; + } + + if (ts.isOptionalChain(node)) { + return transformOptionalChainWithCapture(context, node, thisValueCapture); + } + + const [table, accessExpression] = transformOrderedExpressions(context, [node.expression, node.argumentExpression]); + + const type = context.checker.getTypeAtLocation(node.expression); + const argumentType = context.checker.getTypeAtLocation(node.argumentExpression); + if (isStringType(context, type) && isNumberType(context, argumentType)) { + // strings are not callable, so ignore thisValueCapture + return { + expression: transformLuaLibFunction(context, LuaLibFeature.StringAccess, node, table, accessExpression), + }; + } + + const updatedAccessExpression = addOneToArrayAccessArgument(context, node, accessExpression); + + if (isMultiReturnCall(context, node.expression)) { + const accessType = context.checker.getTypeAtLocation(node.argumentExpression); + if (!isNumberType(context, accessType)) { + context.diagnostics.push(invalidMultiReturnAccess(node)); + } + + const canOmitSelect = ts.isNumericLiteral(node.argumentExpression) && node.argumentExpression.text === "0"; + if (canOmitSelect) { + // wrapping in parenthesis ensures only the first return value is used + // https://www.lua.org/manual/5.1/manual.html#2.5 + return { expression: lua.createParenthesizedExpression(table) }; + } + + const selectIdentifier = lua.createIdentifier("select"); + return { expression: lua.createCallExpression(selectIdentifier, [updatedAccessExpression, table]) }; + } + + if (thisValueCapture) { + const thisValue = captureThisValue(context, table, thisValueCapture, node.expression); + return { + expression: lua.createTableIndexExpression(thisValue, updatedAccessExpression, node), + thisValue, + }; + } + return { expression: lua.createTableIndexExpression(table, updatedAccessExpression, node) }; +} + +export const transformPropertyAccessExpression: FunctionVisitor = (node, context) => + transformPropertyAccessExpressionWithCapture(context, node, undefined).expression; +export function transformPropertyAccessExpressionWithCapture( + context: TransformationContext, + node: ts.PropertyAccessExpression, + thisValueCapture: lua.Identifier | undefined +): ExpressionWithThisValue { + const type = context.checker.getTypeAtLocation(node.expression); + const isOptionalLeft = isOptionalContinuation(node.expression); + + let property = node.name.text; + const symbol = context.checker.getSymbolAtLocation(node.name); + const customName = getCustomNameFromSymbol(context, symbol); + if (customName) { + property = customName; + } + + const constEnumValue = tryGetConstEnumValue(context, node); + if (constEnumValue) { + return { expression: constEnumValue }; + } + + if (ts.isCallExpression(node.expression) && returnsMultiType(context, node.expression)) { + context.diagnostics.push(invalidMultiReturnAccess(node)); + } + + if (ts.isOptionalChain(node)) { + return transformOptionalChainWithCapture(context, node, thisValueCapture); + } + + // Do not output path for member only enums + const annotations = getTypeAnnotations(type); + if (annotations.has(AnnotationKind.CompileMembersOnly)) { + if (isOptionalLeft) { + context.diagnostics.push(unsupportedOptionalCompileMembersOnly(node)); + } + + if (ts.isPropertyAccessExpression(node.expression)) { + // in case of ...x.enum.y transform to ...x.y + const expression = lua.createTableIndexExpression( + context.transformExpression(node.expression.expression), + lua.createStringLiteral(property), + node + ); + return { expression }; + } else { + // Check if we need to account for enum being exported int his file + if ( + isSymbolExported(context, type.symbol) && + getSymbolExportScope(context, type.symbol) === node.expression.getSourceFile() + ) { + return { + expression: lua.createTableIndexExpression( + createExportsIdentifier(), + lua.createStringLiteral(property), + node + ), + }; + } else { + return { expression: lua.createIdentifier(property, node) }; + } + } + } + + const builtinResult = transformBuiltinPropertyAccessExpression(context, node); + if (builtinResult) { + // Ignore thisValueCapture. + // This assumes that nothing returned by builtin property accesses are callable. + // If this assumption is no longer true, this may need to be updated. + return { expression: builtinResult }; + } + + if ( + ts.isIdentifier(node.expression) && + node.parent && + (!ts.isCallExpression(node.parent) || node.parent.expression !== node) + ) { + // Check if this is a method call extension that is not used as a call + const extensionType = getExtensionKindForNode(context, node); + if (extensionType && callExtensions.has(extensionType)) { + context.diagnostics.push(invalidCallExtensionUse(node)); + } + } + + const table = context.transformExpression(node.expression); + + if (thisValueCapture) { + const thisValue = captureThisValue(context, table, thisValueCapture, node.expression); + const expression = lua.createTableIndexExpression(thisValue, lua.createStringLiteral(property), node); + return { + expression, + thisValue, + }; + } + if (node.expression.kind === SyntaxKind.SuperKeyword) { + const symbol = context.checker.getSymbolAtLocation(node); + if (symbol && symbol.flags & ts.SymbolFlags.GetAccessor) { + return { + expression: transformLuaLibFunction( + context, + LuaLibFeature.DescriptorGet, + node, + lua.createIdentifier("self"), + table, + lua.createStringLiteral(property) + ), + }; + } + } + return { expression: lua.createTableIndexExpression(table, lua.createStringLiteral(property), node) }; +} + +export const transformQualifiedName: FunctionVisitor = (node, context) => { + const right = lua.createStringLiteral(node.right.text, node.right); + const left = context.transformExpression(node.left); + + return lua.createTableIndexExpression(left, right, node); +}; diff --git a/src/transformation/visitors/async-await.ts b/src/transformation/visitors/async-await.ts new file mode 100644 index 000000000..8063312c0 --- /dev/null +++ b/src/transformation/visitors/async-await.ts @@ -0,0 +1,34 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { awaitMustBeInAsyncFunction } from "../utils/diagnostics"; +import { importLuaLibFeature, LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { isInAsyncFunction } from "../utils/typescript"; + +export const transformAwaitExpression: FunctionVisitor = (node, context) => { + // Check if await is inside an async function, it is not allowed at top level or in non-async functions + if (!isInAsyncFunction(node)) { + context.diagnostics.push(awaitMustBeInAsyncFunction(node)); + } + + const expression = context.transformExpression(node.expression); + return transformLuaLibFunction(context, LuaLibFeature.Await, node, expression); +}; + +export function isAsyncFunction(declaration: ts.FunctionLikeDeclaration): boolean { + return declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false; +} + +export function wrapInAsyncAwaiter( + context: TransformationContext, + statements: lua.Statement[], + includeResolveParameter = true +): lua.CallExpression { + importLuaLibFeature(context, LuaLibFeature.Await); + + const parameters = includeResolveParameter ? [lua.createIdentifier("____awaiter_resolve")] : []; + + return lua.createCallExpression(lua.createIdentifier("__TS__AsyncAwaiter"), [ + lua.createFunctionExpression(lua.createBlock(statements), parameters), + ]); +} diff --git a/src/transformation/visitors/binary-expression/assignments.ts b/src/transformation/visitors/binary-expression/assignments.ts new file mode 100644 index 000000000..73e6d9dd1 --- /dev/null +++ b/src/transformation/visitors/binary-expression/assignments.ts @@ -0,0 +1,272 @@ +import * as ts from "typescript"; +import { SyntaxKind } from "typescript"; +import * as lua from "../../../LuaAST"; +import { TransformationContext } from "../../context"; +import { validateAssignment } from "../../utils/assignment-validation"; +import { createExportedIdentifier, getDependenciesOfSymbol, isSymbolExported } from "../../utils/export"; +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"; +import { isMultiReturnCall } from "../language-extensions/multi"; +import { cannotAssignToNodeOfKind, notAllowedOptionalAssignment } from "../../utils/diagnostics"; +import { transformElementAccessArgument } from "../access"; +import { moveToPrecedingTemp, transformExpressionList } from "../expression-list"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; + +export function transformAssignmentLeftHandSideExpression( + context: TransformationContext, + node: ts.Expression, + rightHasPrecedingStatements?: boolean +): lua.AssignmentLeftHandSideExpression { + // Access expressions need the components of the left side cached in temps before the right side's preceding statements + if (rightHasPrecedingStatements && (ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node))) { + let table = context.transformExpression(node.expression); + table = moveToPrecedingTemp(context, table, node.expression); + + let index: lua.Expression; + if (ts.isElementAccessExpression(node)) { + index = transformElementAccessArgument(context, node); + index = moveToPrecedingTemp(context, index, node.argumentExpression); + } else { + index = lua.createStringLiteral(node.name.text, node.name); + } + return lua.createTableIndexExpression(table, index, node); + } + + const symbol = context.checker.getSymbolAtLocation(node); + const left = context.transformExpression(node); + + if (lua.isIdentifier(left) && symbol && isSymbolExported(context, symbol)) { + return createExportedIdentifier(context, left); + } + + if (lua.isAssignmentLeftHandSideExpression(left)) { + return left; + } else { + context.diagnostics.push(cannotAssignToNodeOfKind(node, left.kind)); + return lua.createAnonymousIdentifier(); + } +} + +export function transformAssignment( + context: TransformationContext, + // TODO: Change type to ts.LeftHandSideExpression? + lhs: ts.Expression, + right: lua.Expression, + rightHasPrecedingStatements?: boolean, + parent?: ts.Expression +): lua.Statement[] { + if (ts.isOptionalChain(lhs)) { + context.diagnostics.push(notAllowedOptionalAssignment(lhs)); + return []; + } + + if (isArrayLength(context, lhs)) { + const arrayLengthAssignment = lua.createExpressionStatement( + transformLuaLibFunction( + context, + LuaLibFeature.ArraySetLength, + parent, + context.transformExpression(lhs.expression), + right + ) + ); + + return [arrayLengthAssignment]; + } + + if (ts.isPropertyAccessExpression(lhs) || ts.isElementAccessExpression(lhs)) { + if (lhs.expression.kind === SyntaxKind.SuperKeyword) { + const symbol = context.checker.getSymbolAtLocation(lhs); + if (symbol && symbol.flags & ts.SymbolFlags.SetAccessor) { + return [ + lua.createExpressionStatement( + transformLuaLibFunction( + context, + LuaLibFeature.DescriptorSet, + parent, + lua.createIdentifier("self"), + context.transformExpression(lhs.expression), + ts.isPropertyAccessExpression(lhs) + ? lua.createStringLiteral(lhs.name.text) + : context.transformExpression(lhs.argumentExpression), + right + ) + ), + ]; + } + } + } + + const symbol = + lhs.parent && ts.isShorthandPropertyAssignment(lhs.parent) + ? context.checker.getShorthandAssignmentValueSymbol(lhs.parent) + : context.checker.getSymbolAtLocation(lhs); + + const dependentSymbols = symbol ? getDependenciesOfSymbol(context, symbol) : []; + + const left = transformAssignmentLeftHandSideExpression(context, lhs, rightHasPrecedingStatements); + + const rootAssignment = lua.createAssignmentStatement(left, right, lhs.parent); + + return [ + rootAssignment, + ...dependentSymbols.map(symbol => { + const [left] = rootAssignment.left; + const identifierToAssign = createExportedIdentifier(context, lua.createIdentifier(symbol.name)); + return lua.createAssignmentStatement(identifierToAssign, left); + }), + ]; +} + +export function transformAssignmentWithRightPrecedingStatements( + context: TransformationContext, + lhs: ts.Expression, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[], + parent?: ts.Expression +): lua.Statement[] { + return [ + ...rightPrecedingStatements, + ...transformAssignment(context, lhs, right, rightPrecedingStatements.length > 0, parent), + ]; +} + +function transformDestructuredAssignmentExpression( + context: TransformationContext, + expression: ts.DestructuringAssignment +) { + let { precedingStatements: rightPrecedingStatements, result: right } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(expression.right) + ); + context.addPrecedingStatements(rightPrecedingStatements); + if (isMultiReturnCall(context, expression.right)) { + right = wrapInTable(right); + } + + const rightExpr = moveToPrecedingTemp(context, right, expression.right); + const statements = transformDestructuringAssignment( + context, + expression, + rightExpr, + rightPrecedingStatements.length > 0 + ); + + return { statements, result: rightExpr }; +} + +export function transformAssignmentExpression( + context: TransformationContext, + expression: ts.AssignmentExpression +): lua.Expression { + // Validate assignment + const rightType = context.checker.getTypeAtLocation(expression.right); + const leftType = context.checker.getTypeAtLocation(expression.left); + validateAssignment(context, expression.right, rightType, leftType); + + if (isArrayLength(context, expression.left)) { + // array.length = x + return transformLuaLibFunction( + context, + LuaLibFeature.ArraySetLength, + expression, + context.transformExpression(expression.left.expression), + context.transformExpression(expression.right) + ); + } + + if (isDestructuringAssignment(expression)) { + const { statements, result } = transformDestructuredAssignmentExpression(context, expression); + context.addPrecedingStatements(statements); + return result; + } + + if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) { + const { precedingStatements, result: right } = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); + + const left = transformAssignmentLeftHandSideExpression( + context, + expression.left, + precedingStatements.length > 0 + ); + + context.addPrecedingStatements(precedingStatements); + const rightExpr = moveToPrecedingTemp(context, right, expression.right); + context.addPrecedingStatements(lua.createAssignmentStatement(left, rightExpr, expression.left)); + return rightExpr; + } else { + // Simple assignment + // ${left} = ${right}; return ${left} + const left = context.transformExpression(expression.left); + const right = context.transformExpression(expression.right); + context.addPrecedingStatements(transformAssignment(context, expression.left, right)); + return left; + } +} + +const canBeTransformedToLuaAssignmentStatement = ( + context: TransformationContext, + node: ts.DestructuringAssignment +): node is ts.ArrayDestructuringAssignment => + ts.isArrayLiteralExpression(node.left) && + node.left.elements.every(element => { + if (isArrayLength(context, element)) { + return false; + } + + if (ts.isPropertyAccessExpression(element) || ts.isElementAccessExpression(element)) { + // Lua's execution order for multi-assignments is not the same as JS's, so we should always + // break these down when the left side may have side effects. + return false; + } + + if (ts.isIdentifier(element)) { + const symbol = context.checker.getSymbolAtLocation(element); + if (symbol) { + const aliases = getDependenciesOfSymbol(context, symbol); + return aliases.length === 0; + } + } + }); + +export function transformAssignmentStatement( + context: TransformationContext, + expression: ts.AssignmentExpression +): lua.Statement[] { + // Validate assignment + const rightType = context.checker.getTypeAtLocation(expression.right); + const leftType = context.checker.getTypeAtLocation(expression.left); + validateAssignment(context, expression.right, rightType, leftType); + + if (isDestructuringAssignment(expression)) { + if (canBeTransformedToLuaAssignmentStatement(context, expression)) { + const rightType = context.checker.getTypeAtLocation(expression.right); + let right: lua.Expression | lua.Expression[]; + + if (ts.isArrayLiteralExpression(expression.right)) { + right = transformExpressionList(context, expression.right.elements); + } else { + right = context.transformExpression(expression.right); + if (!isMultiReturnCall(context, expression.right) && isArrayType(context, rightType)) { + right = createBoundedUnpackCall(context, right, expression.left.elements.length, expression.right); + } + } + + const left = expression.left.elements.map(e => transformAssignmentLeftHandSideExpression(context, e)); + + return [lua.createAssignmentStatement(left, right, expression)]; + } + + const { statements } = transformDestructuredAssignmentExpression(context, expression); + return statements; + } else { + const { precedingStatements, result: right } = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.right) + ); + return transformAssignmentWithRightPrecedingStatements(context, expression.left, right, precedingStatements); + } +} diff --git a/src/transformation/visitors/binary-expression/bit.ts b/src/transformation/visitors/binary-expression/bit.ts new file mode 100644 index 000000000..79e9c1f3c --- /dev/null +++ b/src/transformation/visitors/binary-expression/bit.ts @@ -0,0 +1,127 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../../CompilerOptions"; +import * as lua from "../../../LuaAST"; +import { assertNever } from "../../../utils"; +import { TransformationContext } from "../../context"; +import { unsupportedForTarget, unsupportedRightShiftOperator } from "../../utils/diagnostics"; + +export type BitOperator = ts.ShiftOperator | ts.BitwiseOperator; +export const isBitOperator = (operator: ts.BinaryOperator): operator is BitOperator => + operator in bitOperatorToLibOperation; + +const bitOperatorToLibOperation: Record = { + [ts.SyntaxKind.AmpersandToken]: "band", + [ts.SyntaxKind.BarToken]: "bor", + [ts.SyntaxKind.CaretToken]: "bxor", + [ts.SyntaxKind.LessThanLessThanToken]: "lshift", + [ts.SyntaxKind.GreaterThanGreaterThanToken]: "arshift", + [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken]: "rshift", +}; + +function transformBinaryBitLibOperation( + node: ts.Node, + left: lua.Expression, + right: lua.Expression, + operator: BitOperator, + lib: string +): lua.Expression { + const functionName = bitOperatorToLibOperation[operator]; + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier(lib), lua.createStringLiteral(functionName)), + [left, right], + node + ); +} + +function transformBitOperatorToLuaOperator( + context: TransformationContext, + node: ts.Node, + operator: BitOperator +): lua.BinaryOperator { + switch (operator) { + case ts.SyntaxKind.BarToken: + return lua.SyntaxKind.BitwiseOrOperator; + case ts.SyntaxKind.CaretToken: + return lua.SyntaxKind.BitwiseExclusiveOrOperator; + case ts.SyntaxKind.AmpersandToken: + return lua.SyntaxKind.BitwiseAndOperator; + case ts.SyntaxKind.LessThanLessThanToken: + return lua.SyntaxKind.BitwiseLeftShiftOperator; + case ts.SyntaxKind.GreaterThanGreaterThanToken: + context.diagnostics.push(unsupportedRightShiftOperator(node)); + return lua.SyntaxKind.BitwiseRightShiftOperator; + case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return lua.SyntaxKind.BitwiseRightShiftOperator; + } +} + +export function transformBinaryBitOperation( + context: TransformationContext, + node: ts.Node, + left: lua.Expression, + right: lua.Expression, + operator: BitOperator +): lua.Expression { + switch (context.luaTarget) { + case LuaTarget.Universal: + case LuaTarget.Lua50: + case LuaTarget.Lua51: + context.diagnostics.push(unsupportedForTarget(node, "Bitwise operations", context.luaTarget)); + return transformBinaryBitLibOperation(node, left, right, operator, "bit"); + + case LuaTarget.LuaJIT: + return transformBinaryBitLibOperation(node, left, right, operator, "bit"); + + case LuaTarget.Lua52: + return transformBinaryBitLibOperation(node, left, right, operator, "bit32"); + default: + const luaOperator = transformBitOperatorToLuaOperator(context, node, operator); + return lua.createBinaryExpression(left, right, luaOperator, node); + } +} + +function transformUnaryBitLibOperation( + node: ts.Node, + expression: lua.Expression, + operator: lua.UnaryBitwiseOperator, + lib: string +): lua.Expression { + let bitFunction: string; + switch (operator) { + case lua.SyntaxKind.BitwiseNotOperator: + bitFunction = "bnot"; + break; + default: + assertNever(operator); + } + + return lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier(lib), lua.createStringLiteral(bitFunction)), + [expression], + node + ); +} + +export function transformUnaryBitOperation( + context: TransformationContext, + node: ts.Node, + expression: lua.Expression, + operator: lua.UnaryBitwiseOperator +): lua.Expression { + switch (context.luaTarget) { + case LuaTarget.Universal: + case LuaTarget.Lua50: + case LuaTarget.Lua51: + context.diagnostics.push(unsupportedForTarget(node, "Bitwise operations", context.luaTarget)); + return transformUnaryBitLibOperation(node, expression, operator, "bit"); + + case LuaTarget.LuaJIT: + return transformUnaryBitLibOperation(node, expression, operator, "bit"); + + case LuaTarget.Lua52: + return transformUnaryBitLibOperation(node, expression, operator, "bit32"); + + default: + return lua.createUnaryExpression(expression, operator, node); + } +} diff --git a/src/transformation/visitors/binary-expression/compound.ts b/src/transformation/visitors/binary-expression/compound.ts new file mode 100644 index 000000000..4532fe56c --- /dev/null +++ b/src/transformation/visitors/binary-expression/compound.ts @@ -0,0 +1,402 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { assertNever } from "../../../utils"; +import { TransformationContext } from "../../context"; +import { transformInPrecedingStatementScope, WithPrecedingStatements } from "../../utils/preceding-statements"; +import { transformBinaryOperation } from "./index"; +import { transformAssignmentWithRightPrecedingStatements } from "./assignments"; +import { isArrayLength } from "./destructuring-assignments"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { cannotAssignToNodeOfKind } from "../../utils/diagnostics"; + +function isLuaExpressionWithSideEffect(expression: lua.Expression) { + return !(lua.isLiteral(expression) || lua.isIdentifier(expression)); +} + +function shouldCacheTableIndexExpressions( + expression: lua.TableIndexExpression, + rightPrecedingStatements: lua.Statement[] +) { + return ( + isLuaExpressionWithSideEffect(expression.table) || + isLuaExpressionWithSideEffect(expression.index) || + rightPrecedingStatements.length > 0 + ); +} + +// TODO: `as const` doesn't work on enum members +type CompoundAssignmentToken = + | ts.SyntaxKind.BarToken + | ts.SyntaxKind.PlusToken + | ts.SyntaxKind.CaretToken + | ts.SyntaxKind.MinusToken + | ts.SyntaxKind.SlashToken + | ts.SyntaxKind.PercentToken + | ts.SyntaxKind.AsteriskToken + | ts.SyntaxKind.AmpersandToken + | ts.SyntaxKind.AsteriskAsteriskToken + | ts.SyntaxKind.LessThanLessThanToken + | ts.SyntaxKind.GreaterThanGreaterThanToken + | ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken + | ts.SyntaxKind.BarBarToken + | ts.SyntaxKind.AmpersandAmpersandToken + | ts.SyntaxKind.QuestionQuestionToken; + +const compoundToAssignmentTokens: Record = { + [ts.SyntaxKind.BarEqualsToken]: ts.SyntaxKind.BarToken, + [ts.SyntaxKind.PlusEqualsToken]: ts.SyntaxKind.PlusToken, + [ts.SyntaxKind.CaretEqualsToken]: ts.SyntaxKind.CaretToken, + [ts.SyntaxKind.MinusEqualsToken]: ts.SyntaxKind.MinusToken, + [ts.SyntaxKind.SlashEqualsToken]: ts.SyntaxKind.SlashToken, + [ts.SyntaxKind.PercentEqualsToken]: ts.SyntaxKind.PercentToken, + [ts.SyntaxKind.AsteriskEqualsToken]: ts.SyntaxKind.AsteriskToken, + [ts.SyntaxKind.AmpersandEqualsToken]: ts.SyntaxKind.AmpersandToken, + [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: ts.SyntaxKind.AsteriskAsteriskToken, + [ts.SyntaxKind.LessThanLessThanEqualsToken]: ts.SyntaxKind.LessThanLessThanToken, + [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: ts.SyntaxKind.GreaterThanGreaterThanToken, + [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, + [ts.SyntaxKind.BarBarEqualsToken]: ts.SyntaxKind.BarBarToken, + [ts.SyntaxKind.AmpersandAmpersandEqualsToken]: ts.SyntaxKind.AmpersandAmpersandToken, + [ts.SyntaxKind.QuestionQuestionEqualsToken]: ts.SyntaxKind.QuestionQuestionToken, +}; + +export const isCompoundAssignmentToken = (token: ts.BinaryOperator): token is ts.CompoundAssignmentOperator => + token in compoundToAssignmentTokens; + +export const unwrapCompoundAssignmentToken = (token: ts.CompoundAssignmentOperator): CompoundAssignmentToken => + compoundToAssignmentTokens[token]; + +function transformCompoundAssignment( + context: TransformationContext, + expression: ts.Expression, + lhs: ts.Expression, + rhs: ts.Expression, + operator: CompoundAssignmentToken, + isPostfix: boolean +): WithPrecedingStatements { + if (isArrayLength(context, lhs)) { + const { precedingStatements, result: lengthSetterStatement } = transformCompoundLengthSetter( + context, + expression, + lhs, + rhs, + operator + ); + + return { precedingStatements, result: lengthSetterStatement.expression }; + } + + const left = context.transformExpression(lhs); + if (!lua.isAssignmentLeftHandSideExpression(left)) { + context.diagnostics.push(cannotAssignToNodeOfKind(expression, left.kind)); + return { precedingStatements: [], result: left }; + } + + const { precedingStatements: rightPrecedingStatements, result: right } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(rhs) + ); + + if (lua.isTableIndexExpression(left)) { + // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects + // local __obj, __index = ${objExpression}, ${indexExpression}; + const obj = context.createTempNameForLuaExpression(left.table); + const index = context.createTempNameForLuaExpression(left.index); + + const objAndIndexDeclaration = lua.createVariableDeclarationStatement([obj, index], [left.table, left.index]); + const accessExpression = lua.createTableIndexExpression(obj, index); + + const tmp = context.createTempNameForLuaExpression(left); + if (isPostfix) { + // local ____tmp = ____obj[____index]; + // ____obj[____index] = ____tmp ${replacementOperator} ${right}; + // return ____tmp + const tmpDeclaration = lua.createVariableDeclarationStatement(tmp, accessExpression); + const { precedingStatements, result: operatorExpression } = transformBinaryOperation( + context, + tmp, + right, + rightPrecedingStatements, + operator, + expression + ); + const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + return { + precedingStatements: [objAndIndexDeclaration, ...precedingStatements, tmpDeclaration, assignStatement], + result: tmp, + }; + } else { + if (isSetterSkippingCompoundAssignmentOperator(operator)) { + return { + precedingStatements: [ + objAndIndexDeclaration, + ...transformSetterSkippingCompoundAssignment( + accessExpression, + operator, + right, + rightPrecedingStatements + ), + ], + result: left, + }; + } + // local ____tmp = ____obj[____index] ${replacementOperator} ${right}; + // ____obj[____index] = ____tmp; + // return ____tmp + const { precedingStatements, result: operatorExpression } = transformBinaryOperation( + context, + accessExpression, + right, + rightPrecedingStatements, + operator, + expression + ); + const tmpDeclaration = lua.createVariableDeclarationStatement(tmp, operatorExpression); + const assignStatement = lua.createAssignmentStatement(accessExpression, tmp); + return { + precedingStatements: [objAndIndexDeclaration, ...precedingStatements, tmpDeclaration, assignStatement], + result: tmp, + }; + } + } else if (isPostfix) { + // Postfix expressions need to cache original value in temp + // local ____tmp = ${left}; + // ${left} = ____tmp ${replacementOperator} ${right}; + // return ____tmp + const tmpIdentifier = context.createTempNameForLuaExpression(left); + const tmpDeclaration = lua.createVariableDeclarationStatement(tmpIdentifier, left); + const { precedingStatements, result: operatorExpression } = transformBinaryOperation( + context, + tmpIdentifier, + right, + rightPrecedingStatements, + operator, + expression + ); + const assignStatements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements + ); + return { + precedingStatements: [tmpDeclaration, ...precedingStatements, ...assignStatements], + result: tmpIdentifier, + }; + } else { + if (rightPrecedingStatements.length > 0 && isSetterSkippingCompoundAssignmentOperator(operator)) { + return { + precedingStatements: transformSetterSkippingCompoundAssignment( + left, + operator, + right, + rightPrecedingStatements + ), + result: left, + }; + } + + // Simple expressions + // ${left} = ${left} ${operator} ${right} + const { precedingStatements, result: operatorExpression } = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + expression + ); + const statements = transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + precedingStatements + ); + return { precedingStatements: statements, result: left }; + } +} + +export function transformCompoundAssignmentExpression( + context: TransformationContext, + expression: ts.Expression, + // TODO: Change type to ts.LeftHandSideExpression? + lhs: ts.Expression, + rhs: ts.Expression, + operator: CompoundAssignmentToken, + isPostfix: boolean +): lua.Expression { + const { precedingStatements, result } = transformCompoundAssignment( + context, + expression, + lhs, + rhs, + operator, + isPostfix + ); + context.addPrecedingStatements(precedingStatements); + return result; +} + +export function transformCompoundAssignmentStatement( + context: TransformationContext, + node: ts.Node, + lhs: ts.Expression, + rhs: ts.Expression, + operator: CompoundAssignmentToken +): lua.Statement[] { + if (isArrayLength(context, lhs)) { + const { precedingStatements, result: lengthSetterStatement } = transformCompoundLengthSetter( + context, + node, + lhs, + rhs, + operator + ); + + return [...precedingStatements, lengthSetterStatement]; + } + + const left = context.transformExpression(lhs); + if (!lua.isAssignmentLeftHandSideExpression(left)) { + context.diagnostics.push(cannotAssignToNodeOfKind(node, left.kind)); + return []; + } + + const { precedingStatements: rightPrecedingStatements, result: right } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(rhs) + ); + + if (lua.isTableIndexExpression(left) && shouldCacheTableIndexExpressions(left, rightPrecedingStatements)) { + // Complex property/element accesses need to cache object/index expressions to avoid repeating side-effects + // local __obj, __index = ${objExpression}, ${indexExpression}; + // ____obj[____index] = ____obj[____index] ${replacementOperator} ${right}; + const obj = context.createTempNameForLuaExpression(left.table); + const index = context.createTempNameForLuaExpression(left.index); + + const objAndIndexDeclaration = lua.createVariableDeclarationStatement([obj, index], [left.table, left.index]); + const accessExpression = lua.createTableIndexExpression(obj, index); + + if (isSetterSkippingCompoundAssignmentOperator(operator)) { + return [ + objAndIndexDeclaration, + ...transformSetterSkippingCompoundAssignment( + accessExpression, + operator, + right, + rightPrecedingStatements, + node + ), + ]; + } + + const { precedingStatements: rightPrecedingStatements2, result: operatorExpression } = transformBinaryOperation( + context, + accessExpression, + right, + rightPrecedingStatements, + operator, + node + ); + const assignStatement = lua.createAssignmentStatement(accessExpression, operatorExpression); + return [objAndIndexDeclaration, ...rightPrecedingStatements2, assignStatement]; + } else { + if (isSetterSkippingCompoundAssignmentOperator(operator)) { + return transformSetterSkippingCompoundAssignment(left, operator, right, rightPrecedingStatements, node); + } + + // Simple statements + // ${left} = ${left} ${replacementOperator} ${right} + const { precedingStatements: rightPrecedingStatements2, result: operatorExpression } = transformBinaryOperation( + context, + left, + right, + rightPrecedingStatements, + operator, + node + ); + return transformAssignmentWithRightPrecedingStatements( + context, + lhs, + operatorExpression, + rightPrecedingStatements2 + ); + } +} + +/* These setter-skipping operators will not execute the setter if result does not change. + * x.y ||= z does NOT call the x.y setter if x.y is already true. + * x.y &&= z does NOT call the x.y setter if x.y is already false. + * x.y ??= z does NOT call the x.y setter if x.y is already not nullish. + */ +type SetterSkippingCompoundAssignmentOperator = ts.LogicalOperator | ts.SyntaxKind.QuestionQuestionToken; + +function isSetterSkippingCompoundAssignmentOperator( + operator: ts.BinaryOperator +): operator is SetterSkippingCompoundAssignmentOperator { + return ( + operator === ts.SyntaxKind.AmpersandAmpersandToken || + operator === ts.SyntaxKind.BarBarToken || + operator === ts.SyntaxKind.QuestionQuestionToken + ); +} + +function transformSetterSkippingCompoundAssignment( + lhs: lua.AssignmentLeftHandSideExpression, + operator: SetterSkippingCompoundAssignmentOperator, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[], + node?: ts.Node +): lua.Statement[] { + // These assignments have the form 'if x then y = z', figure out what condition x is first. + let condition: lua.Expression; + + if (operator === ts.SyntaxKind.AmpersandAmpersandToken) { + condition = lhs; + } else if (operator === ts.SyntaxKind.BarBarToken) { + condition = lua.createUnaryExpression(lhs, lua.SyntaxKind.NotOperator); + } else if (operator === ts.SyntaxKind.QuestionQuestionToken) { + condition = lua.createBinaryExpression(lhs, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator); + } else { + assertNever(operator); + } + + // if condition then lhs = rhs end + return [ + lua.createIfStatement( + condition, + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(lhs, right, node)]), + undefined, + node + ), + ]; +} + +function transformCompoundLengthSetter( + context: TransformationContext, + node: ts.Node, + lhs: ts.PropertyAccessExpression | ts.ElementAccessExpression, + rhs: ts.Expression, + operator: CompoundAssignmentToken +): WithPrecedingStatements { + const { precedingStatements: rightPrecedingStatements, result: right } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(rhs) + ); + const table = context.transformExpression(lhs.expression); + const lengthExpression = lua.createUnaryExpression(table, lua.SyntaxKind.LengthOperator, lhs); + const { precedingStatements, result: operatorExpression } = transformBinaryOperation( + context, + lengthExpression, + right, + rightPrecedingStatements, + operator, + node + ); + + const arrayLengthAssignment = lua.createExpressionStatement( + transformLuaLibFunction(context, LuaLibFeature.ArraySetLength, node, table, operatorExpression) + ); + + return { precedingStatements, result: arrayLengthAssignment }; +} diff --git a/src/transformation/visitors/binary-expression/destructuring-assignments.ts b/src/transformation/visitors/binary-expression/destructuring-assignments.ts new file mode 100644 index 000000000..aa7042727 --- /dev/null +++ b/src/transformation/visitors/binary-expression/destructuring-assignments.ts @@ -0,0 +1,368 @@ +import * as ts from "typescript"; +import { transformBinaryOperation } from "."; +import * as lua from "../../../LuaAST"; +import { assertNever, cast } from "../../../utils"; +import { TransformationContext } from "../../context"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; +import { isArrayType, isAssignmentPattern } from "../../utils/typescript"; +import { moveToPrecedingTemp } from "../expression-list"; +import { transformPropertyName } from "../literal"; +import { + transformAssignment, + transformAssignmentLeftHandSideExpression, + transformAssignmentStatement, +} from "./assignments"; + +export function isArrayLength( + context: TransformationContext, + expression: ts.Expression +): expression is ts.PropertyAccessExpression | ts.ElementAccessExpression { + if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) { + return false; + } + + const type = context.checker.getTypeAtLocation(expression.expression); + if (!isArrayType(context, type)) { + return false; + } + + const name = ts.isPropertyAccessExpression(expression) + ? expression.name.text + : ts.isStringLiteral(expression.argumentExpression) + ? expression.argumentExpression.text + : undefined; + + return name === "length"; +} + +export function transformDestructuringAssignment( + context: TransformationContext, + node: ts.DestructuringAssignment, + root: lua.Expression, + rightHasPrecedingStatements: boolean +): lua.Statement[] { + return transformAssignmentPattern(context, node.left, root, rightHasPrecedingStatements); +} + +export function transformAssignmentPattern( + context: TransformationContext, + node: ts.AssignmentPattern, + root: lua.Expression, + rightHasPrecedingStatements: boolean +): lua.Statement[] { + switch (node.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + return transformObjectLiteralAssignmentPattern(context, node, root, rightHasPrecedingStatements); + case ts.SyntaxKind.ArrayLiteralExpression: + return transformArrayLiteralAssignmentPattern(context, node, root, rightHasPrecedingStatements); + } +} + +function transformArrayLiteralAssignmentPattern( + context: TransformationContext, + node: ts.ArrayLiteralExpression, + root: lua.Expression, + rightHasPrecedingStatements: boolean +): lua.Statement[] { + return node.elements.flatMap((element, index) => { + const indexedRoot = lua.createTableIndexExpression(root, lua.createNumericLiteral(index + 1), element); + + switch (element.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + return transformObjectLiteralAssignmentPattern( + context, + element as ts.ObjectLiteralExpression, + indexedRoot, + rightHasPrecedingStatements + ); + case ts.SyntaxKind.ArrayLiteralExpression: + return transformArrayLiteralAssignmentPattern( + context, + element as ts.ArrayLiteralExpression, + indexedRoot, + rightHasPrecedingStatements + ); + case ts.SyntaxKind.BinaryExpression: + const assignedVariable = context.createTempNameForLuaExpression(indexedRoot); + + const assignedVariableDeclaration = lua.createVariableDeclarationStatement( + assignedVariable, + indexedRoot + ); + + const nilCondition = lua.createBinaryExpression( + assignedVariable, + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator + ); + + const { precedingStatements: defaultPrecedingStatements, result: defaultAssignmentStatements } = + transformInPrecedingStatementScope(context, () => + transformAssignment( + context, + (element as ts.BinaryExpression).left, + context.transformExpression((element as ts.BinaryExpression).right) + ) + ); + + // Keep preceding statements inside if block + defaultAssignmentStatements.unshift(...defaultPrecedingStatements); + + const elseAssignmentStatements = transformAssignment( + context, + (element as ts.BinaryExpression).left, + assignedVariable + ); + + const ifBlock = lua.createBlock(defaultAssignmentStatements); + + const elseBlock = lua.createBlock(elseAssignmentStatements); + + const ifStatement = lua.createIfStatement(nilCondition, ifBlock, elseBlock, node); + + return [assignedVariableDeclaration, ifStatement]; + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + const { precedingStatements, result: statements } = transformInPrecedingStatementScope(context, () => + transformAssignment(context, element, indexedRoot, rightHasPrecedingStatements) + ); + return [...precedingStatements, ...statements]; // Keep preceding statements in order + case ts.SyntaxKind.SpreadElement: + if (index !== node.elements.length - 1) { + // TypeScript error + return []; + } + + const restElements = transformLuaLibFunction( + context, + LuaLibFeature.ArraySlice, + undefined, + root, + lua.createNumericLiteral(index) + ); + + const { precedingStatements: spreadPrecedingStatements, result: spreadStatements } = + transformInPrecedingStatementScope(context, () => + transformAssignment( + context, + (element as ts.SpreadElement).expression, + restElements, + rightHasPrecedingStatements + ) + ); + return [...spreadPrecedingStatements, ...spreadStatements]; // Keep preceding statements in order + case ts.SyntaxKind.OmittedExpression: + return []; + default: + // TypeScript error + return []; + } + }); +} + +function transformObjectLiteralAssignmentPattern( + context: TransformationContext, + node: ts.ObjectLiteralExpression, + root: lua.Expression, + rightHasPrecedingStatements: boolean +): lua.Statement[] { + const result: lua.Statement[] = []; + + for (const property of node.properties) { + switch (property.kind) { + case ts.SyntaxKind.ShorthandPropertyAssignment: + result.push(...transformShorthandPropertyAssignment(context, property, root)); + break; + case ts.SyntaxKind.PropertyAssignment: + result.push(...transformPropertyAssignment(context, property, root, rightHasPrecedingStatements)); + break; + case ts.SyntaxKind.SpreadAssignment: + result.push(...transformSpreadAssignment(context, property, root, node.properties)); + break; + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + // TypeScript error + break; + default: + assertNever(property); + } + } + + return result; +} + +function transformShorthandPropertyAssignment( + context: TransformationContext, + node: ts.ShorthandPropertyAssignment, + root: lua.Expression +): lua.Statement[] { + const result: lua.Statement[] = []; + const assignmentVariableName = transformAssignmentLeftHandSideExpression(context, node.name); + const extractionIndex = lua.createStringLiteral(node.name.text); + const variableExtractionAssignmentStatements = transformAssignment( + context, + node.name, + lua.createTableIndexExpression(root, extractionIndex) + ); + + result.push(...variableExtractionAssignmentStatements); + + const defaultInitializer = node.objectAssignmentInitializer + ? context.transformExpression(node.objectAssignmentInitializer) + : undefined; + + if (defaultInitializer) { + const nilCondition = lua.createBinaryExpression( + assignmentVariableName, + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator + ); + + const assignmentStatements = transformAssignment(context, node.name, defaultInitializer); + + const ifBlock = lua.createBlock(assignmentStatements); + + result.push(lua.createIfStatement(nilCondition, ifBlock, undefined, node)); + } + + return result; +} + +function transformPropertyAssignment( + context: TransformationContext, + node: ts.PropertyAssignment, + root: lua.Expression, + rightHasPrecedingStatements: boolean +): lua.Statement[] { + const result: lua.Statement[] = []; + + if (isAssignmentPattern(node.initializer)) { + const propertyAccessString = transformPropertyName(context, node.name); + const newRootAccess = lua.createTableIndexExpression(root, propertyAccessString); + + if (ts.isObjectLiteralExpression(node.initializer)) { + return transformObjectLiteralAssignmentPattern( + context, + node.initializer, + newRootAccess, + rightHasPrecedingStatements + ); + } + + if (ts.isArrayLiteralExpression(node.initializer)) { + return transformArrayLiteralAssignmentPattern( + context, + node.initializer, + newRootAccess, + rightHasPrecedingStatements + ); + } + } + + context.pushPrecedingStatements(); + + let variableToExtract = transformPropertyName(context, node.name); + // Must be evaluated before left's preceding statements + variableToExtract = moveToPrecedingTemp(context, variableToExtract, node.name); + const extractingExpression = lua.createTableIndexExpression(root, variableToExtract); + + let destructureAssignmentStatements: lua.Statement[]; + if (ts.isBinaryExpression(node.initializer)) { + if ( + ts.isPropertyAccessExpression(node.initializer.left) || + ts.isElementAccessExpression(node.initializer.left) + ) { + // Access expressions need their table and index expressions cached to preserve execution order + const left = cast(context.transformExpression(node.initializer.left), lua.isTableIndexExpression); + + const rightExpression = node.initializer.right; + const { precedingStatements: defaultPrecedingStatements, result: defaultExpression } = + transformInPrecedingStatementScope(context, () => context.transformExpression(rightExpression)); + + const tableTemp = context.createTempNameForLuaExpression(left.table); + const indexTemp = context.createTempNameForLuaExpression(left.index); + + const tempsDeclaration = lua.createVariableDeclarationStatement( + [tableTemp, indexTemp], + [left.table, left.index] + ); + + // obj[index] = extractingExpression ?? defaultExpression + const { precedingStatements: rightPrecedingStatements, result: rhs } = transformBinaryOperation( + context, + extractingExpression, + defaultExpression, + defaultPrecedingStatements, + ts.SyntaxKind.QuestionQuestionToken, + node.initializer + ); + const assignStatement = lua.createAssignmentStatement( + lua.createTableIndexExpression(tableTemp, indexTemp), + rhs + ); + + destructureAssignmentStatements = [tempsDeclaration, ...rightPrecedingStatements, assignStatement]; + } else { + const assignmentLeftHandSide = context.transformExpression(node.initializer.left); + + const nilCondition = lua.createBinaryExpression( + assignmentLeftHandSide, + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator + ); + + const ifBlock = lua.createBlock( + transformAssignmentStatement(context, node.initializer as ts.AssignmentExpression) + ); + + destructureAssignmentStatements = [lua.createIfStatement(nilCondition, ifBlock, undefined, node)]; + } + } else { + destructureAssignmentStatements = transformAssignment( + context, + node.initializer, + extractingExpression, + rightHasPrecedingStatements + ); + } + + result.push(...context.popPrecedingStatements()); + result.push(...destructureAssignmentStatements); + + return result; +} + +function transformSpreadAssignment( + context: TransformationContext, + node: ts.SpreadAssignment, + root: lua.Expression, + properties: ts.NodeArray +): lua.Statement[] { + const usedProperties: lua.TableFieldExpression[] = []; + for (const property of properties) { + if ( + (ts.isShorthandPropertyAssignment(property) || ts.isPropertyAssignment(property)) && + !ts.isComputedPropertyName(property.name) && + !ts.isPrivateIdentifier(property.name) + ) { + const name = ts.isIdentifier(property.name) + ? lua.createStringLiteral(property.name.text) + : context.transformExpression(property.name); + + usedProperties.push(lua.createTableFieldExpression(lua.createBooleanLiteral(true), name)); + } + } + + const extractingExpression = transformLuaLibFunction( + context, + LuaLibFeature.ObjectRest, + undefined, + root, + lua.createTableExpression(usedProperties) + ); + + return transformAssignment(context, node.expression, extractingExpression); +} diff --git a/src/transformation/visitors/binary-expression/index.ts b/src/transformation/visitors/binary-expression/index.ts new file mode 100644 index 000000000..f2b0acb16 --- /dev/null +++ b/src/transformation/visitors/binary-expression/index.ts @@ -0,0 +1,304 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../../CompilerOptions"; +import * as lua from "../../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../../context"; +import { wrapInToStringForConcat } from "../../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { canBeFalsyWhenNotNull, isStandardLibraryType, isStringType } from "../../utils/typescript"; +import { transformTypeOfBinaryExpression } from "../typeof"; +import { transformAssignmentExpression, transformAssignmentStatement } from "./assignments"; +import { BitOperator, isBitOperator, transformBinaryBitOperation } from "./bit"; +import { + isCompoundAssignmentToken, + transformCompoundAssignmentExpression, + transformCompoundAssignmentStatement, + unwrapCompoundAssignmentToken, +} from "./compound"; +import { assert } from "../../../utils"; +import { transformOrderedExpressions } from "../expression-list"; +import { transformInPrecedingStatementScope, WithPrecedingStatements } from "../../utils/preceding-statements"; + +type ShortCircuitOperator = + | ts.SyntaxKind.AmpersandAmpersandToken + | ts.SyntaxKind.BarBarToken + | ts.SyntaxKind.QuestionQuestionToken; + +const isShortCircuitOperator = (value: unknown): value is ShortCircuitOperator => + value === ts.SyntaxKind.AmpersandAmpersandToken || + value === ts.SyntaxKind.BarBarToken || + value === ts.SyntaxKind.QuestionQuestionToken; + +type SimpleOperator = + | ts.AdditiveOperatorOrHigher + | Exclude + | ts.EqualityOperator + | ts.LogicalOperator; + +const simpleOperatorsToLua: Record = { + [ts.SyntaxKind.AmpersandAmpersandToken]: lua.SyntaxKind.AndOperator, + [ts.SyntaxKind.BarBarToken]: lua.SyntaxKind.OrOperator, + [ts.SyntaxKind.PlusToken]: lua.SyntaxKind.AdditionOperator, + [ts.SyntaxKind.MinusToken]: lua.SyntaxKind.SubtractionOperator, + [ts.SyntaxKind.AsteriskToken]: lua.SyntaxKind.MultiplicationOperator, + [ts.SyntaxKind.AsteriskAsteriskToken]: lua.SyntaxKind.PowerOperator, + [ts.SyntaxKind.SlashToken]: lua.SyntaxKind.DivisionOperator, + [ts.SyntaxKind.PercentToken]: lua.SyntaxKind.ModuloOperator, + [ts.SyntaxKind.GreaterThanToken]: lua.SyntaxKind.GreaterThanOperator, + [ts.SyntaxKind.GreaterThanEqualsToken]: lua.SyntaxKind.GreaterEqualOperator, + [ts.SyntaxKind.LessThanToken]: lua.SyntaxKind.LessThanOperator, + [ts.SyntaxKind.LessThanEqualsToken]: lua.SyntaxKind.LessEqualOperator, + [ts.SyntaxKind.EqualsEqualsToken]: lua.SyntaxKind.EqualityOperator, + [ts.SyntaxKind.EqualsEqualsEqualsToken]: lua.SyntaxKind.EqualityOperator, + [ts.SyntaxKind.ExclamationEqualsToken]: lua.SyntaxKind.InequalityOperator, + [ts.SyntaxKind.ExclamationEqualsEqualsToken]: lua.SyntaxKind.InequalityOperator, +}; + +function transformBinaryOperationWithNoPrecedingStatements( + context: TransformationContext, + left: lua.Expression, + right: lua.Expression, + operator: BitOperator | SimpleOperator | ts.SyntaxKind.QuestionQuestionToken, + node: ts.Node +): lua.Expression { + if (isBitOperator(operator)) { + return transformBinaryBitOperation(context, node, left, right, operator); + } + + if (operator === ts.SyntaxKind.QuestionQuestionToken) { + assert(ts.isBinaryExpression(node)); + return transformNullishCoalescingOperationNoPrecedingStatements(context, node, left, right); + } + + if (operator === ts.SyntaxKind.PercentToken && context.luaTarget === LuaTarget.Lua50) { + const mathMod = lua.createTableIndexExpression(lua.createIdentifier("math"), lua.createStringLiteral("mod")); + return lua.createCallExpression(mathMod, [left, right], node); + } + + let luaOperator = simpleOperatorsToLua[operator]; + + // Check if we need to use string concat operator + if (operator === ts.SyntaxKind.PlusToken && ts.isBinaryExpression(node)) { + const typeLeft = context.checker.getTypeAtLocation(node.left); + const typeRight = context.checker.getTypeAtLocation(node.right); + + const isLeftString = isStringType(context, typeLeft); + const isRightString = isStringType(context, typeRight); + if (isLeftString || isRightString) { + left = isLeftString ? left : wrapInToStringForConcat(left); + right = isRightString ? right : wrapInToStringForConcat(right); + luaOperator = lua.SyntaxKind.ConcatOperator; + } + } + + return lua.createBinaryExpression(left, right, luaOperator, node); +} + +export function createShortCircuitBinaryExpressionPrecedingStatements( + context: TransformationContext, + lhs: lua.Expression, + rhs: lua.Expression, + rightPrecedingStatements: lua.Statement[], + operator: ShortCircuitOperator, + node?: ts.BinaryExpression +): WithPrecedingStatements { + const conditionIdentifier = context.createTempNameForLuaExpression(lhs); + const assignmentStatement = lua.createVariableDeclarationStatement(conditionIdentifier, lhs, node?.left); + + let condition: lua.Expression; + switch (operator) { + case ts.SyntaxKind.BarBarToken: + condition = lua.createUnaryExpression( + lua.cloneIdentifier(conditionIdentifier), + lua.SyntaxKind.NotOperator, + node + ); + break; + case ts.SyntaxKind.AmpersandAmpersandToken: + condition = lua.cloneIdentifier(conditionIdentifier); + break; + case ts.SyntaxKind.QuestionQuestionToken: + condition = lua.createBinaryExpression( + lua.cloneIdentifier(conditionIdentifier), + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator, + node + ); + break; + } + + const ifStatement = lua.createIfStatement( + condition, + lua.createBlock([...rightPrecedingStatements, lua.createAssignmentStatement(conditionIdentifier, rhs)]), + undefined, + node?.left + ); + return { precedingStatements: [assignmentStatement, ifStatement], result: conditionIdentifier }; +} + +function transformShortCircuitBinaryExpression( + context: TransformationContext, + node: ts.BinaryExpression, + operator: ShortCircuitOperator +): WithPrecedingStatements { + const lhs = context.transformExpression(node.left); + const { precedingStatements, result } = transformInPrecedingStatementScope(context, () => + context.transformExpression(node.right) + ); + return transformBinaryOperation(context, lhs, result, precedingStatements, operator, node); +} + +export function transformBinaryOperation( + context: TransformationContext, + left: lua.Expression, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[], + operator: BitOperator | SimpleOperator | ts.SyntaxKind.QuestionQuestionToken, + node: ts.Node +): WithPrecedingStatements { + if (rightPrecedingStatements.length > 0 && isShortCircuitOperator(operator)) { + assert(ts.isBinaryExpression(node)); + return createShortCircuitBinaryExpressionPrecedingStatements( + context, + left, + right, + rightPrecedingStatements, + operator, + node + ); + } + + return { + precedingStatements: rightPrecedingStatements, + result: transformBinaryOperationWithNoPrecedingStatements(context, left, right, operator, node), + }; +} + +export const transformBinaryExpression: FunctionVisitor = (node, context) => { + const operator = node.operatorToken.kind; + + const typeOfResult = transformTypeOfBinaryExpression(context, node); + if (typeOfResult) { + return typeOfResult; + } + + if (isCompoundAssignmentToken(operator)) { + const token = unwrapCompoundAssignmentToken(operator); + return transformCompoundAssignmentExpression(context, node, node.left, node.right, token, false); + } + + switch (operator) { + case ts.SyntaxKind.EqualsToken: + return transformAssignmentExpression(context, node as ts.AssignmentExpression); + + case ts.SyntaxKind.InKeyword: { + const lhs = context.transformExpression(node.left); + const rhs = context.transformExpression(node.right); + const indexExpression = lua.createTableIndexExpression(rhs, lhs); + return lua.createBinaryExpression( + indexExpression, + lua.createNilLiteral(), + lua.SyntaxKind.InequalityOperator, + node + ); + } + + case ts.SyntaxKind.InstanceOfKeyword: { + const lhs = context.transformExpression(node.left); + const rhs = context.transformExpression(node.right); + const rhsType = context.checker.getTypeAtLocation(node.right); + + if (isStandardLibraryType(context, rhsType, "ObjectConstructor")) { + return transformLuaLibFunction(context, LuaLibFeature.InstanceOfObject, node, lhs); + } + + return transformLuaLibFunction(context, LuaLibFeature.InstanceOf, node, lhs, rhs); + } + + case ts.SyntaxKind.CommaToken: { + const statements = context.transformStatements(ts.factory.createExpressionStatement(node.left)); + const { precedingStatements, result } = transformInPrecedingStatementScope(context, () => + context.transformExpression(node.right) + ); + statements.push(...precedingStatements); + context.addPrecedingStatements(statements); + return result; + } + + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.BarBarToken: { + const { precedingStatements, result } = transformShortCircuitBinaryExpression(context, node, operator); + context.addPrecedingStatements(precedingStatements); + return result; + } + } + + const { + precedingStatements: orderedExpressionPrecedingStatements, + result: [lhs, rhs], + } = transformInPrecedingStatementScope(context, () => + transformOrderedExpressions(context, [node.left, node.right]) + ); + + const { precedingStatements, result } = transformBinaryOperation( + context, + lhs, + rhs, + orderedExpressionPrecedingStatements, + operator, + node + ); + context.addPrecedingStatements(precedingStatements); + return result; +}; + +export function transformBinaryExpressionStatement( + context: TransformationContext, + node: ts.ExpressionStatement +): lua.Statement[] | lua.Statement | undefined { + const expression = node.expression; + if (!ts.isBinaryExpression(expression)) return; + const operator = expression.operatorToken.kind; + + if (isCompoundAssignmentToken(operator)) { + // +=, -=, etc... + const token = unwrapCompoundAssignmentToken(operator); + return transformCompoundAssignmentStatement(context, expression, expression.left, expression.right, token); + } else if (operator === ts.SyntaxKind.EqualsToken) { + return transformAssignmentStatement(context, expression as ts.AssignmentExpression); + } else if (operator === ts.SyntaxKind.CommaToken) { + const statements = [ + ...context.transformStatements(ts.factory.createExpressionStatement(expression.left)), + ...context.transformStatements(ts.factory.createExpressionStatement(expression.right)), + ]; + + return lua.createDoStatement(statements, expression); + } +} + +function transformNullishCoalescingOperationNoPrecedingStatements( + context: TransformationContext, + node: ts.BinaryExpression, + transformedLeft: lua.Expression, + transformedRight: lua.Expression +): lua.Expression { + const lhsType = context.checker.getTypeAtLocation(node.left); + + // Check if we can take a shortcut to 'lhs or rhs' if the left-hand side cannot be 'false'. + if (canBeFalsyWhenNotNull(context, lhsType)) { + // reuse logic from case with preceding statements + const { precedingStatements, result } = createShortCircuitBinaryExpressionPrecedingStatements( + context, + transformedLeft, + transformedRight, + [], + ts.SyntaxKind.QuestionQuestionToken, + node + ); + context.addPrecedingStatements(precedingStatements); + return result; + } else { + // lhs or rhs + return lua.createBinaryExpression(transformedLeft, transformedRight, lua.SyntaxKind.OrOperator, node); + } +} diff --git a/src/transformation/visitors/block.ts b/src/transformation/visitors/block.ts new file mode 100644 index 000000000..4eef4b6f8 --- /dev/null +++ b/src/transformation/visitors/block.ts @@ -0,0 +1,26 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { performHoisting, Scope, ScopeType } from "../utils/scope"; + +export function transformBlockOrStatement(context: TransformationContext, statement: ts.Statement): lua.Statement[] { + return context.transformStatements(ts.isBlock(statement) ? statement.statements : statement); +} + +export function transformScopeBlock( + context: TransformationContext, + node: ts.Block, + scopeType: ScopeType +): [lua.Block, Scope] { + 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, 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 new file mode 100644 index 000000000..2e64a38eb --- /dev/null +++ b/src/transformation/visitors/break-continue.ts @@ -0,0 +1,46 @@ +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"; + +export const transformBreakStatement: FunctionVisitor = (breakStatement, context) => { + void context; + return lua.createBreakStatement(breakStatement); +}; + +export const transformContinueStatement: FunctionVisitor = (statement, context) => { + const scope = findScope(context, ScopeType.Loop); + + 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 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), + lua.createBreakStatement(statement), + ]; + } +}; diff --git a/src/transformation/visitors/call.ts b/src/transformation/visitors/call.ts new file mode 100644 index 000000000..c4a74135c --- /dev/null +++ b/src/transformation/visitors/call.ts @@ -0,0 +1,293 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { transformBuiltinCallExpression } from "../builtins"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { validateAssignment } from "../utils/assignment-validation"; +import { ContextType, getCallContextType } from "../utils/function-context"; +import { wrapInTable } from "../utils/lua-ast"; +import { isValidLuaIdentifier } from "../utils/safe-names"; +import { isExpressionWithEvaluationEffect } from "../utils/typescript"; +import { transformElementAccessArgument } from "./access"; +import { isMultiReturnCall, shouldMultiReturnCallBeWrapped } from "./language-extensions/multi"; +import { unsupportedBuiltinOptionalCall } from "../utils/diagnostics"; +import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; +import { getOptionalContinuationData, transformOptionalChain } from "./optional-chaining"; +import { transformImportExpression } from "./modules/import"; +import { transformLanguageExtensionCallExpression } from "./language-extensions/call-extension"; +import { getCustomNameFromSymbol } from "./identifier"; + +export function validateArguments( + context: TransformationContext, + params: readonly ts.Expression[], + signature?: ts.Signature +) { + if (!signature || signature.parameters.length < params.length) { + return; + } + for (const [index, param] of params.entries()) { + const signatureParameter = signature.parameters[index]; + if (signatureParameter.valueDeclaration !== undefined) { + const signatureType = context.checker.getTypeAtLocation(signatureParameter.valueDeclaration); + const paramType = context.checker.getTypeAtLocation(param); + validateAssignment(context, param, paramType, signatureType, signatureParameter.name); + } + } +} + +export function transformArguments( + context: TransformationContext, + params: readonly ts.Expression[], + signature?: ts.Signature, + callContext?: ts.Expression +): lua.Expression[] { + validateArguments(context, params, signature); + return transformExpressionList(context, callContext ? [callContext, ...params] : params); +} + +function transformCallWithArguments( + context: TransformationContext, + callExpression: ts.Expression, + transformedArguments: lua.Expression[], + argPrecedingStatements: lua.Statement[], + callContext?: ts.Expression +): [lua.Expression, lua.Expression[]] { + let call = context.transformExpression(callExpression); + + let transformedContext: lua.Expression | undefined; + if (callContext) { + transformedContext = context.transformExpression(callContext); + } + + if (argPrecedingStatements.length > 0) { + if (transformedContext) { + transformedContext = moveToPrecedingTemp(context, transformedContext, callContext); + } + call = moveToPrecedingTemp(context, call, callExpression); + context.addPrecedingStatements(argPrecedingStatements); + } + + if (transformedContext) { + transformedArguments.unshift(transformedContext); + } + + return [call, transformedArguments]; +} + +export function transformCallAndArguments( + context: TransformationContext, + callExpression: ts.Expression, + params: readonly ts.Expression[], + signature?: ts.Signature, + callContext?: ts.Expression +): [lua.Expression, lua.Expression[]] { + const { precedingStatements: argPrecedingStatements, result: transformedArguments } = + transformInPrecedingStatementScope(context, () => transformArguments(context, params, signature, callContext)); + return transformCallWithArguments(context, callExpression, transformedArguments, argPrecedingStatements); +} + +function transformElementAccessCall( + context: TransformationContext, + left: ts.PropertyAccessExpression | ts.ElementAccessExpression, + transformedArguments: lua.Expression[], + argPrecedingStatements: lua.Statement[] +) { + // Cache left-side if it has effects + // local ____self = context; return ____self[argument](parameters); + const selfIdentifier = lua.createIdentifier(context.createTempName("self")); + const callContext = context.transformExpression(left.expression); + const selfAssignment = lua.createVariableDeclarationStatement(selfIdentifier, callContext); + context.addPrecedingStatements(selfAssignment); + + const argument = ts.isElementAccessExpression(left) + ? transformElementAccessArgument(context, left) + : lua.createStringLiteral(left.name.text); + + let index: lua.Expression = lua.createTableIndexExpression(selfIdentifier, argument); + + if (argPrecedingStatements.length > 0) { + // Cache index in temp if args had preceding statements + index = moveToPrecedingTemp(context, index); + context.addPrecedingStatements(argPrecedingStatements); + } + + return lua.createCallExpression(index, [selfIdentifier, ...transformedArguments]); +} + +export function transformContextualCallExpression( + context: TransformationContext, + node: ts.CallExpression | ts.TaggedTemplateExpression, + args: ts.Expression[] | ts.NodeArray +): lua.Expression { + if (ts.isOptionalChain(node)) { + return transformOptionalChain(context, node); + } + const left = ts.isCallExpression(node) ? getCalledExpression(node) : node.tag; + + let { precedingStatements: argPrecedingStatements, result: transformedArguments } = + transformInPrecedingStatementScope(context, () => transformArguments(context, args)); + + if ( + ts.isPropertyAccessExpression(left) && + ts.isIdentifier(left.name) && + isValidLuaIdentifier(left.name.text, context.options) && + argPrecedingStatements.length === 0 + ) { + // table:name() + const table = context.transformExpression(left.expression); + let name = left.name.text; + + const symbol = context.checker.getSymbolAtLocation(left); + const customName = getCustomNameFromSymbol(context, symbol); + + if (customName) { + name = customName; + } + + return lua.createMethodCallExpression(table, lua.createIdentifier(name, left.name), transformedArguments, node); + } else if (ts.isElementAccessExpression(left) || ts.isPropertyAccessExpression(left)) { + if (isExpressionWithEvaluationEffect(left.expression)) { + return transformElementAccessCall(context, left, transformedArguments, argPrecedingStatements); + } else { + let expression: lua.Expression; + [expression, transformedArguments] = transformCallWithArguments( + context, + left, + transformedArguments, + argPrecedingStatements, + left.expression + ); + return lua.createCallExpression(expression, transformedArguments, node); + } + } 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( + context, + left, + transformedArguments, + argPrecedingStatements, + callContext + ); + return lua.createCallExpression(expression, transformedArguments, node); + } else { + throw new Error(`Unsupported LeftHandSideExpression kind: ${ts.SyntaxKind[left.kind]}`); + } +} + +function transformPropertyCall( + context: TransformationContext, + node: ts.CallExpression, + calledMethod: ts.PropertyAccessExpression +): lua.Expression { + const signature = context.checker.getResolvedSignature(node); + + if (calledMethod.expression.kind === ts.SyntaxKind.SuperKeyword) { + // Super calls take the format of super.call(self,...) + const parameters = transformArguments(context, node.arguments, signature, ts.factory.createThis()); + return lua.createCallExpression(context.transformExpression(node.expression), parameters, node); + } + + if (getCallContextType(context, node) !== ContextType.Void) { + // table:name() + return transformContextualCallExpression(context, node, node.arguments); + } else { + // table.name() + const [callPath, parameters] = transformCallAndArguments(context, node.expression, node.arguments, signature); + + return lua.createCallExpression(callPath, parameters, node); + } +} + +function transformElementCall(context: TransformationContext, node: ts.CallExpression): lua.Expression { + if (getCallContextType(context, node) !== ContextType.Void) { + // A contextual parameter must be given to this call expression + return transformContextualCallExpression(context, node, node.arguments); + } else { + // No context + const [expression, parameters] = transformCallAndArguments(context, node.expression, node.arguments); + return lua.createCallExpression(expression, parameters, node); + } +} + +export const transformCallExpression: FunctionVisitor = (node, context) => { + const calledExpression = getCalledExpression(node); + + if (calledExpression.kind === ts.SyntaxKind.ImportKeyword) { + return transformImportExpression(node, context); + } + + if (ts.isOptionalChain(node)) { + return transformOptionalChain(context, node); + } + + const optionalContinuation = ts.isIdentifier(calledExpression) + ? getOptionalContinuationData(calledExpression) + : undefined; + const wrapResultInTable = isMultiReturnCall(context, node) && shouldMultiReturnCallBeWrapped(context, node); + + const builtinOrExtensionResult = + transformBuiltinCallExpression(context, node) ?? transformLanguageExtensionCallExpression(context, node); + if (builtinOrExtensionResult) { + if (optionalContinuation !== undefined) { + context.diagnostics.push(unsupportedBuiltinOptionalCall(node)); + } + return wrapResultInTable ? wrapInTable(builtinOrExtensionResult) : builtinOrExtensionResult; + } + + if (ts.isPropertyAccessExpression(calledExpression)) { + const result = transformPropertyCall(context, node, calledExpression); + return wrapResultInTable ? wrapInTable(result) : result; + } + + if (ts.isElementAccessExpression(calledExpression)) { + const result = transformElementCall(context, node); + return wrapResultInTable ? wrapInTable(result) : result; + } + + const signature = context.checker.getResolvedSignature(node); + + // Handle super calls properly + if (calledExpression.kind === ts.SyntaxKind.SuperKeyword) { + const parameters = transformArguments(context, node.arguments, signature, ts.factory.createThis()); + + return lua.createCallExpression( + lua.createTableIndexExpression( + context.transformExpression(ts.factory.createSuper()), + lua.createStringLiteral("____constructor") + ), + parameters, + node + ); + } + + let callPath: lua.Expression; + let parameters: lua.Expression[]; + + const isContextualCall = getCallContextType(context, node) !== ContextType.Void; + + if (!isContextualCall) { + [callPath, parameters] = transformCallAndArguments(context, calledExpression, node.arguments, signature); + } else { + // if is optionalContinuation, context will be handled by transformOptionalChain. + const useGlobalContext = !context.isStrict && optionalContinuation === undefined; + const callContext = useGlobalContext ? ts.factory.createIdentifier("_G") : ts.factory.createNull(); + [callPath, parameters] = transformCallAndArguments( + context, + calledExpression, + node.arguments, + signature, + callContext + ); + } + + const callExpression = lua.createCallExpression(callPath, parameters, node); + if (optionalContinuation && isContextualCall) { + optionalContinuation.contextualCall = callExpression; + } + return wrapResultInTable ? wrapInTable(callExpression) : callExpression; +}; + +export function getCalledExpression(node: ts.CallExpression): ts.Expression { + return ts.skipOuterExpressions(node.expression); +} diff --git a/src/transformation/visitors/class/decorators.ts b/src/transformation/visitors/class/decorators.ts new file mode 100644 index 000000000..11e4b75c7 --- /dev/null +++ b/src/transformation/visitors/class/decorators.ts @@ -0,0 +1,231 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { TransformationContext } from "../../context"; +import { decoratorInvalidContext, incompleteFieldDecoratorWarning } from "../../utils/diagnostics"; +import { ContextType, getFunctionContextType } from "../../utils/function-context"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { isNonNull } from "../../../utils"; +import { transformMemberExpressionOwnerName, transformMethodName } from "./members/method"; +import { transformPropertyName } from "../literal"; +import { isPrivateNode, isStaticNode } from "./utils"; + +export function transformDecoratorExpression(context: TransformationContext, decorator: ts.Decorator): lua.Expression { + const expression = decorator.expression; + const type = context.checker.getTypeAtLocation(expression); + const callContext = getFunctionContextType(context, type); + if (callContext === ContextType.Void) { + context.diagnostics.push(decoratorInvalidContext(decorator)); + } + + return context.transformExpression(expression); +} + +export function createClassDecoratingExpression( + context: TransformationContext, + classDeclaration: ts.ClassDeclaration | ts.ClassExpression, + className: lua.Expression +): lua.Expression { + const classDecorators = + ts.getDecorators(classDeclaration)?.map(d => transformDecoratorExpression(context, d)) ?? []; + + // If experimentalDecorators flag is set, decorate with legacy decorator logic + if (context.options.experimentalDecorators) { + return createLegacyDecoratingExpression(context, classDeclaration.kind, classDecorators, className); + } + + // Else: TypeScript 5.0 decorator + return createDecoratingExpression(context, className, className, classDecorators, { + kind: lua.createStringLiteral("class"), + name: lua.createStringLiteral(classDeclaration.name?.getText() ?? ""), + }); +} + +export function createClassMethodDecoratingExpression( + context: TransformationContext, + methodDeclaration: ts.MethodDeclaration, + originalMethod: lua.Expression, + className: lua.Identifier +): lua.Expression { + const parameterDecorators = getParameterDecorators(context, methodDeclaration); + const methodDecorators = + ts.getDecorators(methodDeclaration)?.map(d => transformDecoratorExpression(context, d)) ?? []; + + const methodName = transformMethodName(context, methodDeclaration); + + // If experimentalDecorators flag is set, decorate with legacy decorator logic + if (context.options.experimentalDecorators) { + const methodTable = transformMemberExpressionOwnerName(methodDeclaration, className); + return createLegacyDecoratingExpression( + context, + methodDeclaration.kind, + [...methodDecorators, ...parameterDecorators], + methodTable, + methodName + ); + } + + // Else: TypeScript 5.0 decorator + return createDecoratingExpression(context, className, originalMethod, methodDecorators, { + kind: lua.createStringLiteral("method"), + name: methodName, + private: lua.createBooleanLiteral(isPrivateNode(methodDeclaration)), + static: lua.createBooleanLiteral(isStaticNode(methodDeclaration)), + }); +} + +export function createClassAccessorDecoratingExpression( + context: TransformationContext, + accessor: ts.AccessorDeclaration, + originalAccessor: lua.Expression, + className: lua.Identifier +): lua.Expression { + const accessorDecorators = ts.getDecorators(accessor)?.map(d => transformDecoratorExpression(context, d)) ?? []; + const propertyName = transformPropertyName(context, accessor.name); + + // If experimentalDecorators flag is set, decorate with legacy decorator logic + if (context.options.experimentalDecorators) { + const propertyOwnerTable = transformMemberExpressionOwnerName(accessor, className); + + return createLegacyDecoratingExpression( + context, + accessor.kind, + accessorDecorators, + propertyOwnerTable, + propertyName + ); + } + + // Else: TypeScript 5.0 decorator + return createDecoratingExpression(context, className, originalAccessor, accessorDecorators, { + kind: lua.createStringLiteral(accessor.kind === ts.SyntaxKind.SetAccessor ? "setter" : "getter"), + name: propertyName, + private: lua.createBooleanLiteral(isPrivateNode(accessor)), + static: lua.createBooleanLiteral(isStaticNode(accessor)), + }); +} + +export function createClassPropertyDecoratingExpression( + context: TransformationContext, + property: ts.PropertyDeclaration, + className: lua.Identifier +): lua.Expression { + const decorators = ts.getDecorators(property) ?? []; + const propertyDecorators = decorators.map(d => transformDecoratorExpression(context, d)); + + // If experimentalDecorators flag is set, decorate with legacy decorator logic + if (context.options.experimentalDecorators) { + const propertyName = transformPropertyName(context, property.name); + const propertyOwnerTable = transformMemberExpressionOwnerName(property, className); + + return createLegacyDecoratingExpression( + context, + property.kind, + propertyDecorators, + propertyOwnerTable, + propertyName + ); + } + + // Else: TypeScript 5.0 decorator + + // Add a diagnostic when something is returned from a field decorator + for (const decorator of decorators) { + const signature = context.checker.getResolvedSignature(decorator); + const decoratorReturnType = signature?.getReturnType(); + // If return type of decorator is NOT void + if (decoratorReturnType && (decoratorReturnType.flags & ts.TypeFlags.Void) === 0) { + context.diagnostics.push(incompleteFieldDecoratorWarning(property)); + } + } + + return createDecoratingExpression(context, className, lua.createNilLiteral(), propertyDecorators, { + kind: lua.createStringLiteral("field"), + name: lua.createStringLiteral(property.name.getText()), + private: lua.createBooleanLiteral(isPrivateNode(property)), + static: lua.createBooleanLiteral(isStaticNode(property)), + }); +} + +function createDecoratingExpression( + context: TransformationContext, + className: lua.Expression, + originalValue: TValue, + decorators: lua.Expression[], + decoratorContext: Record +): lua.Expression { + const decoratorTable = lua.createTableExpression(decorators.map(d => lua.createTableFieldExpression(d))); + const decoratorContextTable = objectToLuaTableLiteral(decoratorContext); + + return transformLuaLibFunction( + context, + LuaLibFeature.Decorate, + undefined, + className, + originalValue, + decoratorTable, + decoratorContextTable + ); +} + +function objectToLuaTableLiteral(obj: Record): lua.Expression { + return lua.createTableExpression( + Object.entries(obj).map(([key, value]) => lua.createTableFieldExpression(value, lua.createStringLiteral(key))) + ); +} + +// Legacy decorators: +function createLegacyDecoratingExpression( + context: TransformationContext, + kind: ts.SyntaxKind, + decorators: lua.Expression[], + targetTableName: lua.Expression, + targetFieldExpression?: lua.Expression +): lua.Expression { + const decoratorTable = lua.createTableExpression(decorators.map(e => lua.createTableFieldExpression(e))); + const trailingExpressions = [decoratorTable, targetTableName]; + + if (targetFieldExpression) { + trailingExpressions.push(targetFieldExpression); + const isMethodOrAccessor = + kind === ts.SyntaxKind.MethodDeclaration || + kind === ts.SyntaxKind.GetAccessor || + kind === ts.SyntaxKind.SetAccessor; + trailingExpressions.push(isMethodOrAccessor ? lua.createBooleanLiteral(true) : lua.createNilLiteral()); + } + + return transformLuaLibFunction(context, LuaLibFeature.DecorateLegacy, undefined, ...trailingExpressions); +} + +function getParameterDecorators( + context: TransformationContext, + node: ts.FunctionLikeDeclarationBase +): lua.CallExpression[] { + return node.parameters + .flatMap((parameter, index) => + ts + .getDecorators(parameter) + ?.map(decorator => + transformLuaLibFunction( + context, + LuaLibFeature.DecorateParam, + node, + lua.createNumericLiteral(index), + transformDecoratorExpression(context, decorator) + ) + ) + ) + .filter(isNonNull); +} + +export function createConstructorDecoratingExpression( + context: TransformationContext, + node: ts.ConstructorDeclaration, + className: lua.Identifier +): lua.Statement | undefined { + const parameterDecorators = getParameterDecorators(context, node); + + if (parameterDecorators.length > 0) { + const decorateMethod = createLegacyDecoratingExpression(context, node.kind, parameterDecorators, className); + return lua.createExpressionStatement(decorateMethod); + } +} diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts new file mode 100644 index 000000000..f2024babe --- /dev/null +++ b/src/transformation/visitors/class/index.ts @@ -0,0 +1,285 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { AllAccessorDeclarations, FunctionVisitor, TransformationContext } from "../../context"; +import { + createDefaultExportExpression, + createExportedIdentifier, + hasDefaultExportModifier, + isSymbolExported, + shouldBeExported, +} from "../../utils/export"; +import { createSelfIdentifier } from "../../utils/lua-ast"; +import { createSafeName, isUnsafeName } from "../../utils/safe-names"; +import { transformIdentifier } from "../identifier"; +import { createClassDecoratingExpression, createConstructorDecoratingExpression } from "./decorators"; +import { transformAccessorDeclarations } from "./members/accessors"; +import { createConstructorName, transformConstructorDeclaration } from "./members/constructor"; +import { transformClassInstanceFields, transformStaticPropertyDeclaration } from "./members/fields"; +import { transformMethodDeclaration } from "./members/method"; +import { getExtendedNode, getExtendedType, isStaticNode } from "./utils"; +import { createClassSetup } from "./setup"; +import { LuaTarget } from "../../../CompilerOptions"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; +import { createClassPropertyDecoratingExpression } from "./decorators"; +import { findFirstNodeAbove } from "../../utils/typescript"; + +export const transformClassDeclaration: FunctionVisitor = (declaration, context) => { + // If declaration is a default export, transform to export variable assignment instead + if (hasDefaultExportModifier(declaration)) { + // Class declaration including assignment to ____exports.default are in preceding statements + const { precedingStatements } = transformInPrecedingStatementScope(context, () => { + transformClassAsExpression(declaration, context); + return []; + }); + return precedingStatements; + } + + const { statements } = transformClassLikeDeclaration(declaration, context); + return statements; +}; + +export const transformThisExpression: FunctionVisitor = node => createSelfIdentifier(node); + +export function transformClassAsExpression( + expression: ts.ClassLikeDeclaration, + context: TransformationContext +): lua.Expression { + const { statements, name } = transformClassLikeDeclaration(expression, context); + context.addPrecedingStatements(statements); + return name; +} + +/** @internal */ +export interface ClassSuperInfo { + className: lua.Identifier; + extendedTypeNode?: ts.ExpressionWithTypeArguments; +} + +function transformClassLikeDeclaration( + classDeclaration: ts.ClassLikeDeclaration, + context: TransformationContext, + nameOverride?: lua.Identifier +): { statements: lua.Statement[]; name: lua.Identifier } { + let className: lua.Identifier; + if (nameOverride !== undefined) { + className = nameOverride; + } else if (classDeclaration.name !== undefined) { + className = transformIdentifier(context, classDeclaration.name); + } else { + className = lua.createIdentifier(context.createTempName("class"), classDeclaration); + } + + // Get type that is extended + const extendedTypeNode = getExtendedNode(classDeclaration); + const extendedType = getExtendedType(context, classDeclaration); + + context.classSuperInfos.push({ className, extendedTypeNode }); + + // Get all properties with value + const properties = classDeclaration.members.filter(ts.isPropertyDeclaration).filter(member => member.initializer); + + // Divide properties into static and non-static + const instanceFields = properties.filter(prop => !isStaticNode(prop)); + + const result: lua.Statement[] = []; + + let localClassName: lua.Identifier; + if (isUnsafeName(className.text, context.options)) { + localClassName = lua.createIdentifier( + createSafeName(className.text), + undefined, + className.symbolId, + className.text + ); + lua.setNodePosition(localClassName, className); + } else { + localClassName = className; + } + + result.push(...createClassSetup(context, classDeclaration, className, localClassName, extendedType)); + + // Find first constructor with body + const constructor = classDeclaration.members.find( + (n): n is ts.ConstructorDeclaration => ts.isConstructorDeclaration(n) && n.body !== undefined + ); + + if (constructor) { + // Add constructor plus initialization of instance fields + const constructorResult = transformConstructorDeclaration( + context, + constructor, + localClassName, + instanceFields, + classDeclaration + ); + + if (constructorResult) result.push(constructorResult); + + // Legacy constructor decorator + const decoratingExpression = createConstructorDecoratingExpression(context, constructor, localClassName); + if (decoratingExpression) result.push(decoratingExpression); + } else if (!extendedType) { + // Generate a constructor if none was defined in a base class + const constructorResult = transformConstructorDeclaration( + context, + ts.factory.createConstructorDeclaration([], [], ts.factory.createBlock([], true)), + localClassName, + instanceFields, + classDeclaration + ); + + if (constructorResult) result.push(constructorResult); + } else if (instanceFields.length > 0) { + // Generate a constructor if none was defined in a class with instance fields that need initialization + // localClassName.prototype.____constructor = function(self, ...) + // baseClassName.prototype.____constructor(self, ...) // or unpack(arg) for Lua 5.0 + // ... + const constructorBody = transformClassInstanceFields(context, instanceFields); + const argsExpression = + context.luaTarget === LuaTarget.Lua50 + ? lua.createCallExpression(lua.createIdentifier("unpack"), [lua.createArgLiteral()]) + : lua.createDotsLiteral(); + const superCall = lua.createExpressionStatement( + lua.createCallExpression( + lua.createTableIndexExpression( + context.transformExpression(ts.factory.createSuper()), + lua.createStringLiteral("____constructor") + ), + [createSelfIdentifier(), argsExpression] + ) + ); + constructorBody.unshift(superCall); + const constructorFunction = lua.createFunctionExpression( + lua.createBlock(constructorBody), + [createSelfIdentifier()], + lua.createDotsLiteral(), + lua.NodeFlags.Declaration + ); + result.push( + lua.createAssignmentStatement(createConstructorName(localClassName), constructorFunction, classDeclaration) + ); + } + + // Transform class members + + // First transform the methods, in case the static properties call them + for (const member of classDeclaration.members) { + if (ts.isMethodDeclaration(member)) { + // Methods + const statements = transformMethodDeclaration(context, member, localClassName); + result.push(...statements); + } + } + + // Then transform the rest + for (const member of classDeclaration.members) { + if (ts.isAccessor(member)) { + // Accessors + const symbol = context.checker.getSymbolAtLocation(member.name); + if (!symbol) continue; + const accessors = getAllAccessorDeclarations(classDeclaration, symbol, context); + if (accessors.firstAccessor !== member) continue; + + const accessorsResult = transformAccessorDeclarations(context, accessors, localClassName); + if (accessorsResult) { + result.push(accessorsResult); + } + } else if (ts.isPropertyDeclaration(member)) { + // Properties + if (isStaticNode(member)) { + const statement = transformStaticPropertyDeclaration(context, member, localClassName); + if (statement) result.push(statement); + } + + if (ts.getDecorators(member)?.length) { + result.push( + lua.createExpressionStatement(createClassPropertyDecoratingExpression(context, member, className)) + ); + } + } else if (ts.isClassStaticBlockDeclaration(member)) { + if (member.body.statements.length > 0) { + const bodyStatements = context.transformStatements(member.body.statements); + const iif = lua.createFunctionExpression(lua.createBlock(bodyStatements), [ + lua.createIdentifier("self"), + ]); + const iife = lua.createCallExpression(iif, [localClassName]); + result.push(lua.createExpressionStatement(iife, member)); + } + } + } + + // Decorate the class + if (ts.canHaveDecorators(classDeclaration) && ts.getDecorators(classDeclaration)) { + const decoratingExpression = createClassDecoratingExpression(context, classDeclaration, localClassName); + const decoratingStatement = lua.createAssignmentStatement(localClassName, decoratingExpression); + result.push(decoratingStatement); + + if (shouldBeExported(classDeclaration)) { + const exportExpression = hasDefaultExportModifier(classDeclaration) + ? createDefaultExportExpression(classDeclaration) + : createExportedIdentifier(context, localClassName); + + const classAssignment = lua.createAssignmentStatement(exportExpression, localClassName); + result.push(classAssignment); + } + } + + context.classSuperInfos.pop(); + + return { statements: result, name: className }; +} + +function getAllAccessorDeclarations( + classDeclaration: ts.ClassLikeDeclaration, + symbol: ts.Symbol, + context: TransformationContext +): AllAccessorDeclarations { + const getAccessor = classDeclaration.members.find( + (m): m is ts.GetAccessorDeclaration => + ts.isGetAccessor(m) && context.checker.getSymbolAtLocation(m.name) === symbol + ); + const setAccessor = classDeclaration.members.find( + (m): m is ts.SetAccessorDeclaration => + ts.isSetAccessor(m) && context.checker.getSymbolAtLocation(m.name) === symbol + ); + + // Get the first of the two (that is not undefined) + const firstAccessor = + getAccessor && (!setAccessor || getAccessor.pos < setAccessor.pos) ? getAccessor : setAccessor!; + + return { + firstAccessor, + setAccessor, + getAccessor, + }; +} + +export const transformSuperExpression: FunctionVisitor = (expression, context) => { + const superInfos = context.classSuperInfos; + const superInfo = superInfos[superInfos.length - 1]; + if (!superInfo) return lua.createAnonymousIdentifier(expression); + const { className, extendedTypeNode } = superInfo; + + // Using `super` without extended type node is a TypeScript error + const extendsExpression = extendedTypeNode?.expression; + let baseClassName: lua.AssignmentLeftHandSideExpression | undefined; + + if (extendsExpression && ts.isIdentifier(extendsExpression)) { + const symbol = context.checker.getSymbolAtLocation(extendsExpression); + if (symbol && !isSymbolExported(context, symbol)) { + // Use "baseClassName" if base is a simple identifier + baseClassName = transformIdentifier(context, extendsExpression); + } + } + + // 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)) { + // In static method, don't add prototype to super call + return baseClassName; + } else { + return lua.createTableIndexExpression(baseClassName, lua.createStringLiteral("prototype")); + } +}; diff --git a/src/transformation/visitors/class/members/accessors.ts b/src/transformation/visitors/class/members/accessors.ts new file mode 100644 index 000000000..017100056 --- /dev/null +++ b/src/transformation/visitors/class/members/accessors.ts @@ -0,0 +1,58 @@ +import * as ts from "typescript"; +import * as lua from "../../../../LuaAST"; +import { AllAccessorDeclarations, TransformationContext } from "../../../context"; +import { createSelfIdentifier } from "../../../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../../../utils/lualib"; +import { transformFunctionBody, transformParameters } from "../../function"; +import { transformPropertyName } from "../../literal"; +import { isStaticNode } from "../utils"; +import { createPrototypeName } from "./constructor"; +import { createClassAccessorDecoratingExpression } from "../decorators"; + +function transformAccessor( + context: TransformationContext, + node: ts.AccessorDeclaration, + className: lua.Identifier +): lua.Expression { + const [params, dot, restParam] = transformParameters(context, node.parameters, createSelfIdentifier()); + const body = node.body ? transformFunctionBody(context, node.parameters, node.body, node, restParam)[0] : []; + const accessorFunction = lua.createFunctionExpression( + lua.createBlock(body), + params, + dot, + lua.NodeFlags.Declaration + ); + + if (ts.getDecorators(node)?.length) { + return createClassAccessorDecoratingExpression(context, node, accessorFunction, className); + } else { + return accessorFunction; + } +} + +export function transformAccessorDeclarations( + context: TransformationContext, + { firstAccessor, getAccessor, setAccessor }: AllAccessorDeclarations, + className: lua.Identifier +): lua.Statement | undefined { + const propertyName = transformPropertyName(context, firstAccessor.name); + const descriptor = lua.createTableExpression([]); + + if (getAccessor) { + const getterFunction = transformAccessor(context, getAccessor, className); + descriptor.fields.push(lua.createTableFieldExpression(getterFunction, lua.createStringLiteral("get"))); + } + + if (setAccessor) { + const setterFunction = transformAccessor(context, setAccessor, className); + descriptor.fields.push(lua.createTableFieldExpression(setterFunction, lua.createStringLiteral("set"))); + } + + const isStatic = isStaticNode(firstAccessor); + const target = isStatic ? lua.cloneIdentifier(className) : createPrototypeName(className); + const feature = isStatic ? LuaLibFeature.ObjectDefineProperty : LuaLibFeature.SetDescriptor; + const parameters: lua.Expression[] = [target, propertyName, descriptor]; + if (!isStatic) parameters.push(lua.createBooleanLiteral(true)); + const call = transformLuaLibFunction(context, feature, undefined, ...parameters); + return lua.createExpressionStatement(call); +} diff --git a/src/transformation/visitors/class/members/constructor.ts b/src/transformation/visitors/class/members/constructor.ts new file mode 100644 index 000000000..8bff2f4fc --- /dev/null +++ b/src/transformation/visitors/class/members/constructor.ts @@ -0,0 +1,95 @@ +import * as ts from "typescript"; +import * as lua from "../../../../LuaAST"; +import { TransformationContext } from "../../../context"; +import { createSelfIdentifier } from "../../../utils/lua-ast"; +import { ScopeType } from "../../../utils/scope"; +import { transformFunctionBodyContent, transformFunctionBodyHeader, transformParameters } from "../../function"; +import { transformIdentifier } from "../../identifier"; +import { transformClassInstanceFields } from "./fields"; + +export function createPrototypeName(className: lua.Identifier): lua.TableIndexExpression { + return lua.createTableIndexExpression(lua.cloneIdentifier(className), lua.createStringLiteral("prototype")); +} + +export function createConstructorName(className: lua.Identifier): lua.TableIndexExpression { + return lua.createTableIndexExpression(createPrototypeName(className), lua.createStringLiteral("____constructor")); +} + +export function transformConstructorDeclaration( + context: TransformationContext, + statement: ts.ConstructorDeclaration, + className: lua.Identifier, + instanceFields: ts.PropertyDeclaration[], + classDeclaration: ts.ClassLikeDeclaration +): lua.Statement | undefined { + // Don't transform methods without body (overload declarations) + if (!statement.body) { + return undefined; + } + + // Transform body + const scope = context.pushScope(ScopeType.Function, statement); + const body = transformFunctionBodyContent(context, statement.body); + + const [params, dotsLiteral, restParamName] = transformParameters( + context, + statement.parameters, + createSelfIdentifier() + ); + + // Make sure default parameters are assigned before fields are initialized + const bodyWithFieldInitializers = transformFunctionBodyHeader(context, scope, statement.parameters, restParamName); + + // Check for field declarations in constructor + const constructorFieldsDeclarations = statement.parameters.filter(p => p.modifiers !== undefined); + + const classInstanceFields = transformClassInstanceFields(context, instanceFields); + + // If there are field initializers and there is a super call somewhere, + // move super call and everything before it to between default assignments and initializers + if ( + (constructorFieldsDeclarations.length > 0 || classInstanceFields.length > 0) && + statement.body && + statement.body.statements.length > 0 + ) { + const superIndex = statement.body.statements.findIndex( + s => + ts.isExpressionStatement(s) && + ts.isCallExpression(s.expression) && + s.expression.expression.kind === ts.SyntaxKind.SuperKeyword + ); + + if (superIndex !== -1) { + bodyWithFieldInitializers.push(...body.splice(0, superIndex + 1)); + } + } + + // Add in instance field declarations + for (const declaration of constructorFieldsDeclarations) { + if (ts.isIdentifier(declaration.name)) { + // self.declarationName = declarationName + const assignment = lua.createAssignmentStatement( + lua.createTableIndexExpression(createSelfIdentifier(), lua.createStringLiteral(declaration.name.text)), + transformIdentifier(context, declaration.name) + ); + bodyWithFieldInitializers.push(assignment); + } + // else { TypeScript error: A parameter property may not be declared using a binding pattern } + } + + bodyWithFieldInitializers.push(...classInstanceFields); + + bodyWithFieldInitializers.push(...body); + + const block = lua.createBlock(bodyWithFieldInitializers); + + const constructorWasGenerated = statement.pos === -1; + + context.popScope(); + + return lua.createAssignmentStatement( + createConstructorName(className), + lua.createFunctionExpression(block, params, dotsLiteral, lua.NodeFlags.Declaration), + constructorWasGenerated ? classDeclaration : statement + ); +} diff --git a/src/transformation/visitors/class/members/fields.ts b/src/transformation/visitors/class/members/fields.ts new file mode 100644 index 000000000..2308eaac8 --- /dev/null +++ b/src/transformation/visitors/class/members/fields.ts @@ -0,0 +1,47 @@ +import * as ts from "typescript"; +import * as lua from "../../../../LuaAST"; +import { TransformationContext } from "../../../context"; +import { createSelfIdentifier } from "../../../utils/lua-ast"; +import { transformInPrecedingStatementScope } from "../../../utils/preceding-statements"; +import { transformPropertyName } from "../../literal"; + +export function transformClassInstanceFields( + context: TransformationContext, + instanceFields: ts.PropertyDeclaration[] +): lua.Statement[] { + const statements: lua.Statement[] = []; + + for (const f of instanceFields) { + const { precedingStatements, result: statement } = transformInPrecedingStatementScope(context, () => { + // Get identifier + const fieldName = transformPropertyName(context, f.name); + + const value = f.initializer ? context.transformExpression(f.initializer) : undefined; + + // self[fieldName] + const selfIndex = lua.createTableIndexExpression(createSelfIdentifier(), fieldName); + + // self[fieldName] = value + const assignClassField = lua.createAssignmentStatement(selfIndex, value, f); + + return assignClassField; + }); + + statements.push(...precedingStatements, statement); + } + + return statements; +} + +export function transformStaticPropertyDeclaration( + context: TransformationContext, + field: ts.PropertyDeclaration, + className: lua.Identifier +): lua.AssignmentStatement | undefined { + if (!field.initializer) return; + const fieldName = transformPropertyName(context, field.name); + const value = context.transformExpression(field.initializer); + const classField = lua.createTableIndexExpression(lua.cloneIdentifier(className), fieldName); + + return lua.createAssignmentStatement(classField, value); +} diff --git a/src/transformation/visitors/class/members/method.ts b/src/transformation/visitors/class/members/method.ts new file mode 100644 index 000000000..adfe2c352 --- /dev/null +++ b/src/transformation/visitors/class/members/method.ts @@ -0,0 +1,70 @@ +import * as ts from "typescript"; +import * as lua from "../../../../LuaAST"; +import { TransformationContext } from "../../../context"; +import { transformFunctionToExpression } from "../../function"; +import { transformPropertyName } from "../../literal"; +import { isStaticNode } from "../utils"; +import { createPrototypeName } from "./constructor"; +import { createClassMethodDecoratingExpression } from "../decorators"; + +export function transformMemberExpressionOwnerName( + node: ts.PropertyDeclaration | ts.MethodDeclaration | ts.AccessorDeclaration, + className: lua.Identifier +): lua.Expression { + return isStaticNode(node) ? lua.cloneIdentifier(className) : createPrototypeName(className); +} + +export function transformMethodName(context: TransformationContext, node: ts.MethodDeclaration): lua.Expression { + const methodName = transformPropertyName(context, node.name); + if (lua.isStringLiteral(methodName) && methodName.value === "toString") { + return lua.createStringLiteral("__tostring", node.name); + } + return methodName; +} + +export function transformMethodDeclaration( + context: TransformationContext, + node: ts.MethodDeclaration, + className: lua.Identifier +): lua.Statement[] { + // Don't transform methods without body (overload declarations) + if (!node.body) return []; + + const methodTable = transformMemberExpressionOwnerName(node, className); + const methodName = transformMethodName(context, node); + const [functionExpression] = transformFunctionToExpression(context, node); + + const methodHasDecorators = (ts.getDecorators(node)?.length ?? 0) > 0; + const methodHasParameterDecorators = node.parameters.some(p => (ts.getDecorators(p)?.length ?? 0) > 0); // Legacy decorators + + if (methodHasDecorators || methodHasParameterDecorators) { + if (context.options.experimentalDecorators) { + // Legacy decorator statement + return [ + lua.createAssignmentStatement( + lua.createTableIndexExpression(methodTable, methodName), + functionExpression + ), + lua.createExpressionStatement( + createClassMethodDecoratingExpression(context, node, functionExpression, className) + ), + ]; + } else { + return [ + lua.createAssignmentStatement( + lua.createTableIndexExpression(methodTable, methodName), + createClassMethodDecoratingExpression(context, node, functionExpression, className), + node + ), + ]; + } + } else { + return [ + lua.createAssignmentStatement( + lua.createTableIndexExpression(methodTable, methodName), + functionExpression, + node + ), + ]; + } +} diff --git a/src/transformation/visitors/class/new.ts b/src/transformation/visitors/class/new.ts new file mode 100644 index 000000000..0c5733451 --- /dev/null +++ b/src/transformation/visitors/class/new.ts @@ -0,0 +1,67 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { FunctionVisitor } from "../../context"; +import { AnnotationKind, getTypeAnnotations } from "../../utils/annotations"; +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); + + const type = context.checker.getTypeAtLocation(node); + const annotations = getTypeAnnotations(type); + const customConstructorAnnotation = annotations.get(AnnotationKind.CustomConstructor); + if (customConstructorAnnotation) { + if (customConstructorAnnotation.args.length === 1) { + return lua.createCallExpression( + lua.createIdentifier(customConstructorAnnotation.args[0]), + transformArguments(context, node.arguments ?? []), + node + ); + } else { + context.diagnostics.push( + annotationInvalidArgumentCount( + node, + AnnotationKind.CustomConstructor, + customConstructorAnnotation.args.length, + 1 + ) + ); + } + } + + return transformLuaLibFunction(context, LuaLibFeature.New, node, name, ...params); +}; diff --git a/src/transformation/visitors/class/setup.ts b/src/transformation/visitors/class/setup.ts new file mode 100644 index 000000000..ab6eeea92 --- /dev/null +++ b/src/transformation/visitors/class/setup.ts @@ -0,0 +1,99 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { assert } from "../../../utils"; +import { TransformationContext } from "../../context"; +import { + createDefaultExportStringLiteral, + createExportedIdentifier, + getIdentifierExportScope, + hasDefaultExportModifier, +} from "../../utils/export"; +import { createExportsIdentifier, createLocalOrExportedOrGlobalDeclaration } from "../../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { getExtendedNode, getExtendsClause } from "./utils"; + +export function createClassSetup( + context: TransformationContext, + statement: ts.ClassLikeDeclarationBase, + className: lua.Identifier, + localClassName: lua.Identifier, + extendsType?: ts.Type +): lua.Statement[] { + const result: lua.Statement[] = []; + + // __TS__Class() + const classInitializer = transformLuaLibFunction(context, LuaLibFeature.Class, statement); + + const defaultExportLeftHandSide = hasDefaultExportModifier(statement) + ? lua.createTableIndexExpression(createExportsIdentifier(), createDefaultExportStringLiteral(statement)) + : undefined; + + // [____exports.]className = __TS__Class() + if (defaultExportLeftHandSide) { + result.push(lua.createAssignmentStatement(defaultExportLeftHandSide, classInitializer, statement)); + } else { + result.push(...createLocalOrExportedOrGlobalDeclaration(context, className, classInitializer, statement)); + } + + if (defaultExportLeftHandSide) { + // local localClassName = ____exports.default + result.push(lua.createVariableDeclarationStatement(localClassName, defaultExportLeftHandSide)); + } else { + const exportScope = getIdentifierExportScope(context, className); + if (exportScope) { + // local localClassName = ____exports.className + result.push( + lua.createVariableDeclarationStatement( + localClassName, + createExportedIdentifier(context, lua.cloneIdentifier(className), exportScope) + ) + ); + } + } + + // localClassName.name = className + result.push( + lua.createAssignmentStatement( + lua.createTableIndexExpression(lua.cloneIdentifier(localClassName), lua.createStringLiteral("name")), + getReflectionClassName(statement, className), + statement + ) + ); + + if (extendsType) { + const extendedNode = getExtendedNode(statement); + assert(extendedNode); + result.push( + lua.createExpressionStatement( + transformLuaLibFunction( + context, + LuaLibFeature.ClassExtends, + getExtendsClause(statement), + lua.cloneIdentifier(localClassName), + context.transformExpression(extendedNode.expression) + ) + ) + ); + } + + return result; +} + +export function getReflectionClassName( + declaration: ts.ClassLikeDeclarationBase, + className: lua.Identifier +): lua.Expression { + if (declaration.name) { + return lua.createStringLiteral(declaration.name.text); + } else if (ts.isVariableDeclaration(declaration.parent) && ts.isIdentifier(declaration.parent.name)) { + return lua.createStringLiteral(declaration.parent.name.text); + } else if (hasDefaultExportModifier(declaration)) { + return lua.createStringLiteral("default"); + } + + if (getExtendedNode(declaration)) { + return lua.createTableIndexExpression(className, lua.createStringLiteral("name")); + } + + return lua.createStringLiteral(""); +} diff --git a/src/transformation/visitors/class/utils.ts b/src/transformation/visitors/class/utils.ts new file mode 100644 index 000000000..0cd4384bb --- /dev/null +++ b/src/transformation/visitors/class/utils.ts @@ -0,0 +1,29 @@ +import * as ts from "typescript"; +import { TransformationContext } from "../../context"; + +export function isPrivateNode(node: ts.HasModifiers): boolean { + return node.modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword) === true; +} + +export function isStaticNode(node: ts.HasModifiers): boolean { + return node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword) === true; +} + +export function getExtendsClause(node: ts.ClassLikeDeclarationBase): ts.HeritageClause | undefined { + return node.heritageClauses?.find(clause => clause.token === ts.SyntaxKind.ExtendsKeyword); +} + +export function getExtendedNode(node: ts.ClassLikeDeclarationBase): ts.ExpressionWithTypeArguments | undefined { + const extendsClause = getExtendsClause(node); + if (!extendsClause) return; + + return extendsClause.types[0]; +} + +export function getExtendedType( + context: TransformationContext, + node: ts.ClassLikeDeclarationBase +): ts.Type | undefined { + const extendedNode = getExtendedNode(node); + return extendedNode && context.checker.getTypeAtLocation(extendedNode); +} diff --git a/src/transformation/visitors/conditional.ts b/src/transformation/visitors/conditional.ts new file mode 100644 index 000000000..6285a488d --- /dev/null +++ b/src/transformation/visitors/conditional.ts @@ -0,0 +1,131 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { transformInPrecedingStatementScope, WithPrecedingStatements } from "../utils/preceding-statements"; +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, + expression: ts.ConditionalExpression, + condition: WithPrecedingStatements, + whenTrue: WithPrecedingStatements, + whenFalse: WithPrecedingStatements +): lua.Expression { + const tempVar = context.createTempNameForNode(expression.condition); + + const trueStatements = whenTrue.precedingStatements.concat( + lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), whenTrue.result, expression.whenTrue) + ); + + const falseStatements = whenFalse.precedingStatements.concat( + lua.createAssignmentStatement(lua.cloneIdentifier(tempVar), whenFalse.result, expression.whenFalse) + ); + + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(tempVar, undefined, expression.condition), + ...condition.precedingStatements, + lua.createIfStatement( + condition.result, + lua.createBlock(trueStatements, expression.whenTrue), + lua.createBlock(falseStatements, expression.whenFalse), + expression + ), + ]); + return lua.cloneIdentifier(tempVar); +} + +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); + + const condition = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.condition) + ); + const whenTrue = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.whenTrue) + ); + const whenFalse = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression.whenFalse) + ); + if ( + whenTrue.precedingStatements.length > 0 || + whenFalse.precedingStatements.length > 0 || + canBeFalsy(context, context.checker.getTypeAtLocation(expression.whenTrue)) + ) { + return transformProtectedConditionalExpression(context, expression, condition, whenTrue, whenFalse); + } + + // condition and v1 or v2 + context.addPrecedingStatements(condition.precedingStatements); + const conditionAnd = lua.createBinaryExpression(condition.result, whenTrue.result, lua.SyntaxKind.AndOperator); + return lua.createBinaryExpression(conditionAnd, whenFalse.result, lua.SyntaxKind.OrOperator, expression); +}; + +export function transformIfStatement(statement: ts.IfStatement, context: TransformationContext): lua.IfStatement { + context.pushScope(ScopeType.Conditional, statement); + + // Check if we need to add diagnostic about Lua truthiness + checkOnlyTruthyCondition(statement.expression, context); + + const condition = context.transformExpression(statement.expression); + const statements = performHoisting(context, transformBlockOrStatement(context, statement.thenStatement)); + context.popScope(); + const ifBlock = lua.createBlock(statements); + + if (statement.elseStatement) { + if (ts.isIfStatement(statement.elseStatement)) { + const tsElseStatement = statement.elseStatement; + const { precedingStatements, result: elseStatement } = transformInPrecedingStatementScope(context, () => + transformIfStatement(tsElseStatement, context) + ); + // If else-if condition generates preceding statements, we can't use elseif, we have to break it down: + // if conditionA then + // ... + // else + // conditionB's preceding statements + // if conditionB then + // end + // end + if (precedingStatements.length > 0) { + const elseBlock = lua.createBlock([...precedingStatements, elseStatement]); + return lua.createIfStatement(condition, ifBlock, elseBlock); + } else { + return lua.createIfStatement(condition, ifBlock, elseStatement); + } + } else { + context.pushScope(ScopeType.Conditional, statement); + const elseStatements = performHoisting( + context, + transformBlockOrStatement(context, statement.elseStatement) + ); + context.popScope(); + const elseBlock = lua.createBlock(elseStatements); + return lua.createIfStatement(condition, ifBlock, elseBlock); + } + } + + return lua.createIfStatement(condition, ifBlock); +} + +export function checkOnlyTruthyCondition(condition: ts.Expression, context: TransformationContext) { + if (context.options.strictNullChecks === false) return; // This check is not valid if everything could implicitly be nil + if (ts.isElementAccessExpression(condition)) return; // Array index could always implicitly return nil + + if (!canBeFalsy(context, context.checker.getTypeAtLocation(condition))) { + context.diagnostics.push(truthyOnlyConditionalValue(condition)); + } +} diff --git a/src/transformation/visitors/delete.ts b/src/transformation/visitors/delete.ts new file mode 100644 index 000000000..efa879c2e --- /dev/null +++ b/src/transformation/visitors/delete.ts @@ -0,0 +1,40 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor } from "../context"; +import { transformLuaLibFunction, LuaLibFeature } from "../utils/lualib"; +import { unsupportedProperty } from "../utils/diagnostics"; +import { isArrayType, isNumberType } from "../utils/typescript"; +import { addToNumericExpression } from "../utils/lua-ast"; +import { transformOptionalDeleteExpression } from "./optional-chaining"; + +export const transformDeleteExpression: FunctionVisitor = (node, context) => { + if (ts.isOptionalChain(node.expression)) { + return transformOptionalDeleteExpression(context, node, node.expression); + } + + let ownerExpression: lua.Expression | undefined; + let propertyExpression: lua.Expression | undefined; + + if (ts.isPropertyAccessExpression(node.expression)) { + if (ts.isPrivateIdentifier(node.expression.name)) throw new Error("PrivateIdentifier is not supported"); + ownerExpression = context.transformExpression(node.expression.expression); + propertyExpression = lua.createStringLiteral(node.expression.name.text); + } else if (ts.isElementAccessExpression(node.expression)) { + ownerExpression = context.transformExpression(node.expression.expression); + propertyExpression = context.transformExpression(node.expression.argumentExpression); + + const type = context.checker.getTypeAtLocation(node.expression.expression); + const argumentType = context.checker.getTypeAtLocation(node.expression.argumentExpression); + + if (isArrayType(context, type) && isNumberType(context, argumentType)) { + propertyExpression = addToNumericExpression(propertyExpression, 1); + } + } + + if (!ownerExpression || !propertyExpression) { + context.diagnostics.push(unsupportedProperty(node, "delete", ts.SyntaxKind[node.kind])); + return lua.createNilLiteral(); + } + + return transformLuaLibFunction(context, LuaLibFeature.Delete, node, ownerExpression, propertyExpression); +}; diff --git a/src/transformation/visitors/enum.ts b/src/transformation/visitors/enum.ts new file mode 100644 index 000000000..9de658a45 --- /dev/null +++ b/src/transformation/visitors/enum.ts @@ -0,0 +1,95 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { AnnotationKind, getTypeAnnotations } from "../utils/annotations"; +import { addExportToIdentifier, getSymbolExportScope } from "../utils/export"; +import { createLocalOrExportedOrGlobalDeclaration } from "../utils/lua-ast"; +import { isFirstDeclaration } from "../utils/typescript"; +import { transformIdentifier } from "./identifier"; +import { transformPropertyName } from "./literal"; + +export function tryGetConstEnumValue( + context: TransformationContext, + node: ts.EnumMember | ts.PropertyAccessExpression | ts.ElementAccessExpression +): lua.Expression | undefined { + const value = context.checker.getConstantValue(node); + if (typeof value === "string") { + return lua.createStringLiteral(value, node); + } else if (typeof value === "number") { + return lua.createNumericLiteral(value, node); + } +} + +export const transformEnumDeclaration: FunctionVisitor = (node, context) => { + if (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Const && !context.options.preserveConstEnums) { + return undefined; + } + + const type = context.checker.getTypeAtLocation(node); + const membersOnly = getTypeAnnotations(type).has(AnnotationKind.CompileMembersOnly); + const result: lua.Statement[] = []; + + if (!membersOnly && isFirstDeclaration(context, node)) { + const name = transformIdentifier(context, node.name); + const table = lua.createBinaryExpression( + addExportToIdentifier(context, name), + lua.createTableExpression(), + lua.SyntaxKind.OrOperator + ); + result.push(...createLocalOrExportedOrGlobalDeclaration(context, name, table, node)); + } + + const enumReference = context.transformExpression(node.name); + for (const member of node.members) { + const memberName = transformPropertyName(context, member.name); + + let valueExpression: lua.Expression | undefined; + const constEnumValue = tryGetConstEnumValue(context, member); + if (constEnumValue) { + valueExpression = constEnumValue; + } else if (member.initializer) { + if (ts.isIdentifier(member.initializer)) { + const symbol = context.checker.getSymbolAtLocation(member.initializer); + if ( + symbol?.valueDeclaration && + ts.isEnumMember(symbol.valueDeclaration) && + symbol.valueDeclaration.parent === node + ) { + const otherMemberName = transformPropertyName(context, symbol.valueDeclaration.name); + valueExpression = lua.createTableIndexExpression(enumReference, otherMemberName); + } + } + + valueExpression ??= context.transformExpression(member.initializer); + } else { + valueExpression = lua.createNilLiteral(); + } + + if (membersOnly) { + const enumSymbol = context.checker.getSymbolAtLocation(node.name); + const exportScope = enumSymbol ? getSymbolExportScope(context, enumSymbol) : undefined; + + result.push( + ...createLocalOrExportedOrGlobalDeclaration( + context, + lua.isIdentifier(memberName) + ? memberName + : lua.createIdentifier(member.name.getText(), member.name), + valueExpression, + node, + exportScope + ) + ); + } else { + const memberAccessor = lua.createTableIndexExpression(enumReference, memberName); + result.push(lua.createAssignmentStatement(memberAccessor, valueExpression, member)); + + if (!lua.isStringLiteral(valueExpression) && !lua.isNilLiteral(valueExpression)) { + const reverseMemberAccessor = lua.createTableIndexExpression(enumReference, memberAccessor); + result.push(lua.createAssignmentStatement(reverseMemberAccessor, memberName, member)); + } + } + } + + return result; +}; diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts new file mode 100644 index 000000000..f81519e4c --- /dev/null +++ b/src/transformation/visitors/errors.ts @@ -0,0 +1,204 @@ +import * as ts from "typescript"; +import { LuaLibFeature, LuaTarget } from "../.."; +import * as lua from "../../LuaAST"; +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 { isInAsyncFunction, isInGeneratorFunction } from "../utils/typescript"; +import { wrapInAsyncAwaiter } from "./async-await"; +import { transformScopeBlock } from "./block"; +import { transformIdentifier } from "./identifier"; +import { isInMultiReturnFunction } from "./language-extensions/multi"; +import { createReturnStatement } from "./return"; + +const transformAsyncTry: FunctionVisitor = (statement, context) => { + const [tryBlock] = transformScopeBlock(context, statement.tryBlock, ScopeType.Try); + + if ( + (context.options.luaTarget === LuaTarget.Lua50 || context.options.luaTarget === LuaTarget.Lua51) && + !context.options.lua51AllowTryCatchInAsyncAwait + ) { + context.diagnostics.push( + unsupportedForTargetButOverrideAvailable( + statement, + "try/catch inside async functions", + LuaTarget.Lua51, + "lua51AllowTryCatchInAsyncAwait" + ) + ); + return tryBlock.statements; + } + + // __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]; + + if (statement.finallyBlock) { + const awaiterFinally = lua.createTableIndexExpression(awaiterIdentifier, lua.createStringLiteral("finally")); + const finallyFunction = lua.createFunctionExpression( + lua.createBlock(context.transformStatements(statement.finallyBlock.statements)) + ); + const finallyCall = lua.createCallExpression( + awaiterFinally, + [awaiterIdentifier, finallyFunction], + statement.finallyBlock + ); + // ____try.finally() + result.push(lua.createExpressionStatement(finallyCall)); + } + + if (statement.catchClause) { + // ____try.catch() + const [catchFunction] = transformCatchClause(context, statement.catchClause); + if (catchFunction.params) { + catchFunction.params.unshift(lua.createAnonymousIdentifier()); + } + + const awaiterCatch = lua.createTableIndexExpression(awaiterIdentifier, lua.createStringLiteral("catch")); + const catchCall = lua.createCallExpression(awaiterCatch, [awaiterIdentifier, catchFunction]); + + // 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)); + } + + return result; +}; + +export const transformTryStatement: FunctionVisitor = (statement, context) => { + if (isInAsyncFunction(statement)) { + return transformAsyncTry(statement, context); + } + + const [tryBlock, tryScope] = transformScopeBlock(context, statement.tryBlock, ScopeType.Try); + + if ( + (context.options.luaTarget === LuaTarget.Lua50 || context.options.luaTarget === LuaTarget.Lua51) && + isInGeneratorFunction(statement) + ) { + context.diagnostics.push( + unsupportedForTarget(statement, "try/catch inside generator functions", LuaTarget.Lua51) + ); + return tryBlock.statements; + } + + const tryResultIdentifier = lua.createIdentifier("____try"); + const returnValueIdentifier = lua.createIdentifier("____returnValue"); + + const result: lua.Statement[] = []; + + const returnedIdentifier = lua.createIdentifier("____hasReturned"); + let returnCondition: lua.Expression | undefined; + + const pCall = lua.createIdentifier("pcall"); + const tryCall = lua.createCallExpression(pCall, [lua.createFunctionExpression(tryBlock)]); + + if (statement.catchClause && statement.catchClause.block.statements.length > 0) { + // try with catch + const [catchFunction, catchScope] = transformCatchClause(context, statement.catchClause); + const catchIdentifier = lua.createIdentifier("____catch"); + result.push(lua.createVariableDeclarationStatement(catchIdentifier, catchFunction)); + + const hasReturn = tryScope.functionReturned ?? catchScope.functionReturned; + + const tryReturnIdentifiers = [tryResultIdentifier]; // ____try + if (hasReturn || statement.catchClause.variableDeclaration) { + tryReturnIdentifiers.push(returnedIdentifier); // ____returned + if (hasReturn) { + tryReturnIdentifiers.push(returnValueIdentifier); // ____returnValue + returnCondition = lua.cloneIdentifier(returnedIdentifier); + } + } + result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall)); + + const catchCall = lua.createCallExpression( + catchIdentifier, + statement.catchClause.variableDeclaration ? [lua.cloneIdentifier(returnedIdentifier)] : [] + ); + const catchCallStatement = hasReturn + ? lua.createAssignmentStatement( + [lua.cloneIdentifier(returnedIdentifier), lua.cloneIdentifier(returnValueIdentifier)], + catchCall + ) + : lua.createExpressionStatement(catchCall); + + const notTryCondition = lua.createUnaryExpression(tryResultIdentifier, lua.SyntaxKind.NotOperator); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([catchCallStatement]))); + } else if (tryScope.functionReturned) { + // try with return, but no catch + // returnedIdentifier = lua.createIdentifier("____returned"); + const returnedVariables = [tryResultIdentifier, returnedIdentifier, returnValueIdentifier]; + result.push(lua.createVariableDeclarationStatement(returnedVariables, tryCall)); + + // change return condition from '____returned' to '____try and ____returned' + returnCondition = lua.createBinaryExpression( + lua.cloneIdentifier(tryResultIdentifier), + returnedIdentifier, + lua.SyntaxKind.AndOperator + ); + } else { + // try without return or catch + result.push(lua.createExpressionStatement(tryCall)); + } + + if (statement.finallyBlock && statement.finallyBlock.statements.length > 0) { + result.push(...context.transformStatements(statement.finallyBlock)); + } + + if (returnCondition && returnedIdentifier) { + const returnValues: lua.Expression[] = []; + + if (isInMultiReturnFunction(context, statement)) { + returnValues.push(createUnpackCall(context, lua.cloneIdentifier(returnValueIdentifier))); + } else { + returnValues.push(lua.cloneIdentifier(returnValueIdentifier)); + } + + const returnStatement = createReturnStatement(context, returnValues, statement); + const ifReturnedStatement = lua.createIfStatement(returnCondition, lua.createBlock([returnStatement])); + result.push(ifReturnedStatement); + } + + return lua.createDoStatement(result, statement); +}; + +export const transformThrowStatement: FunctionVisitor = (statement, context) => { + const parameters: lua.Expression[] = []; + + if (statement.expression) { + parameters.push(context.transformExpression(statement.expression)); + parameters.push(lua.createNumericLiteral(0)); + } + + return lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), parameters), + statement + ); +}; + +function transformCatchClause( + context: TransformationContext, + catchClause: ts.CatchClause +): [lua.FunctionExpression, Scope] { + const [catchBlock, catchScope] = transformScopeBlock(context, catchClause.block, ScopeType.Catch); + + const catchParameter = catchClause.variableDeclaration + ? transformIdentifier(context, catchClause.variableDeclaration.name as ts.Identifier) + : undefined; + const catchFunction = lua.createFunctionExpression( + catchBlock, + catchParameter ? [lua.cloneIdentifier(catchParameter)] : [] + ); + + return [catchFunction, catchScope]; +} diff --git a/src/transformation/visitors/expression-list.ts b/src/transformation/visitors/expression-list.ts new file mode 100644 index 000000000..617d09d4a --- /dev/null +++ b/src/transformation/visitors/expression-list.ts @@ -0,0 +1,197 @@ +import assert = require("assert"); +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { TransformationContext, tempSymbolId } from "../context"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; +import { isConstIdentifier } from "../utils/typescript"; +import { isOptionalContinuation } from "./optional-chaining"; + +export function shouldMoveToTemp(context: TransformationContext, expression: lua.Expression, tsOriginal?: ts.Node) { + return ( + !lua.isLiteral(expression) && + !(lua.isIdentifier(expression) && expression.symbolId === tempSymbolId) && // Treat generated temps as consts + !( + tsOriginal && + (isConstIdentifier(context, tsOriginal) || + isOptionalContinuation(tsOriginal) || + tsOriginal.kind === ts.SyntaxKind.ThisKeyword) + ) + ); +} + +// Cache an expression in a preceding statement and return the temp identifier +export function moveToPrecedingTemp( + context: TransformationContext, + expression: lua.Expression, + tsOriginal?: ts.Node +): lua.Expression { + if (!shouldMoveToTemp(context, expression, tsOriginal)) { + return expression; + } + const tempIdentifier = context.createTempNameForLuaExpression(expression); + const tempDeclaration = lua.createVariableDeclarationStatement(tempIdentifier, expression, tsOriginal); + context.addPrecedingStatements(tempDeclaration); + return lua.cloneIdentifier(tempIdentifier, tsOriginal); +} + +function transformExpressions( + context: TransformationContext, + expressions: readonly ts.Expression[] +): { + transformedExpressions: lua.Expression[]; + precedingStatements: lua.Statement[][]; + lastPrecedingStatementsIndex: number; +} { + const precedingStatements: lua.Statement[][] = []; + const transformedExpressions: lua.Expression[] = []; + let lastPrecedingStatementsIndex = -1; + for (let i = 0; i < expressions.length; ++i) { + const { precedingStatements: expressionPrecedingStatements, result: expression } = + transformInPrecedingStatementScope(context, () => context.transformExpression(expressions[i])); + transformedExpressions.push(expression); + if (expressionPrecedingStatements.length > 0) { + lastPrecedingStatementsIndex = i; + } + precedingStatements.push(expressionPrecedingStatements); + } + return { transformedExpressions, precedingStatements, lastPrecedingStatementsIndex }; +} + +function transformExpressionsUsingTemps( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + precedingStatements: lua.Statement[][], + lastPrecedingStatementsIndex: number +) { + for (let i = 0; i < transformedExpressions.length; ++i) { + context.addPrecedingStatements(precedingStatements[i]); + if (i < lastPrecedingStatementsIndex) { + transformedExpressions[i] = moveToPrecedingTemp(context, transformedExpressions[i], expressions[i]); + } + } + return transformedExpressions; +} + +function pushToSparseArray( + context: TransformationContext, + arrayIdentifier: lua.Identifier | undefined, + expressions: lua.Expression[] +) { + if (!arrayIdentifier) { + arrayIdentifier = lua.createIdentifier(context.createTempName("array")); + const libCall = transformLuaLibFunction(context, LuaLibFeature.SparseArrayNew, undefined, ...expressions); + const declaration = lua.createVariableDeclarationStatement(arrayIdentifier, libCall); + context.addPrecedingStatements(declaration); + } else { + const libCall = transformLuaLibFunction( + context, + LuaLibFeature.SparseArrayPush, + undefined, + arrayIdentifier, + ...expressions + ); + context.addPrecedingStatements(lua.createExpressionStatement(libCall)); + } + return arrayIdentifier; +} + +function transformExpressionsUsingSparseArray( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + precedingStatements: lua.Statement[][] +) { + let arrayIdentifier: lua.Identifier | undefined; + + let expressionBatch: lua.Expression[] = []; + for (let i = 0; i < expressions.length; ++i) { + // Expressions with preceding statements should always be at the start of a batch + if (precedingStatements[i].length > 0 && expressionBatch.length > 0) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + expressionBatch = []; + } + + context.addPrecedingStatements(precedingStatements[i]); + expressionBatch.push(transformedExpressions[i]); + + // Spread expressions should always be at the end of a batch + if (ts.isSpreadElement(expressions[i])) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + expressionBatch = []; + } + } + + if (expressionBatch.length > 0) { + arrayIdentifier = pushToSparseArray(context, arrayIdentifier, expressionBatch); + } + + assert(arrayIdentifier); + return [transformLuaLibFunction(context, LuaLibFeature.SparseArraySpread, undefined, arrayIdentifier)]; +} + +function countNeededTemps( + context: TransformationContext, + expressions: readonly ts.Expression[], + transformedExpressions: lua.Expression[], + lastPrecedingStatementsIndex: number +) { + if (lastPrecedingStatementsIndex < 0) { + return 0; + } + return transformedExpressions + .slice(0, lastPrecedingStatementsIndex) + .filter((e, i) => shouldMoveToTemp(context, e, expressions[i])).length; +} + +// Transforms a list of expressions while flattening spreads and maintaining execution order +export function transformExpressionList( + context: TransformationContext, + expressions: readonly ts.Expression[] +): lua.Expression[] { + const { transformedExpressions, precedingStatements, lastPrecedingStatementsIndex } = transformExpressions( + context, + expressions + ); + + // If more than this number of temps are required to preserve execution order, we'll fall back to using the + // sparse array lib functions instead to prevent excessive locals. + const maxTemps = 2; + + // Use sparse array lib if there are spreads before the last expression + // or if too many temps are needed to preserve order + const lastSpread = expressions.findIndex(e => ts.isSpreadElement(e)); + if ( + (lastSpread >= 0 && lastSpread < expressions.length - 1) || + countNeededTemps(context, expressions, transformedExpressions, lastPrecedingStatementsIndex) > maxTemps + ) { + return transformExpressionsUsingSparseArray(context, expressions, transformedExpressions, precedingStatements); + } else { + return transformExpressionsUsingTemps( + context, + expressions, + transformedExpressions, + precedingStatements, + lastPrecedingStatementsIndex + ); + } +} + +// Transforms a series of expressions while maintaining execution order +export function transformOrderedExpressions( + context: TransformationContext, + expressions: readonly ts.Expression[] +): lua.Expression[] { + const { transformedExpressions, precedingStatements, lastPrecedingStatementsIndex } = transformExpressions( + context, + expressions + ); + return transformExpressionsUsingTemps( + context, + expressions, + transformedExpressions, + precedingStatements, + lastPrecedingStatementsIndex + ); +} diff --git a/src/transformation/visitors/expression-statement.ts b/src/transformation/visitors/expression-statement.ts new file mode 100644 index 000000000..a247fd29b --- /dev/null +++ b/src/transformation/visitors/expression-statement.ts @@ -0,0 +1,36 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, tempSymbolId } from "../context"; +import { transformBinaryExpressionStatement } from "./binary-expression"; +import { transformUnaryExpressionStatement } from "./unary-expression"; + +export const transformExpressionStatement: FunctionVisitor = (node, context) => { + const unaryExpressionResult = transformUnaryExpressionStatement(context, node); + if (unaryExpressionResult) { + return unaryExpressionResult; + } + + const binaryExpressionResult = transformBinaryExpressionStatement(context, node); + if (binaryExpressionResult) { + return binaryExpressionResult; + } + + return wrapInStatement(context.transformExpression(node.expression)); +}; + +export function wrapInStatement(result: lua.Expression): lua.Statement | undefined { + const isTempVariable = lua.isIdentifier(result) && result.symbolId === tempSymbolId; + if (isTempVariable) { + return undefined; + } + // "synthetic": no side effects and no original source + const isSyntheticExpression = (lua.isIdentifier(result) || lua.isLiteral(result)) && result.line === undefined; + if (isSyntheticExpression) { + return undefined; + } + if (lua.isCallExpression(result) || lua.isMethodCallExpression(result)) { + return lua.createExpressionStatement(result); + } + // Assign expression statements to dummy to make sure they're legal Lua + return lua.createVariableDeclarationStatement(lua.createAnonymousIdentifier(), result); +} diff --git a/src/transformation/visitors/function.ts b/src/transformation/visitors/function.ts new file mode 100644 index 000000000..12b152353 --- /dev/null +++ b/src/transformation/visitors/function.ts @@ -0,0 +1,369 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { assert } from "../../utils"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { createDefaultExportStringLiteral, hasDefaultExportModifier } from "../utils/export"; +import { ContextType, getFunctionContextType } from "../utils/function-context"; +import { getExtensionKindForType } from "../utils/language-extensions"; +import { + createExportsIdentifier, + createLocalOrExportedOrGlobalDeclaration, + createSelfIdentifier, + wrapInTable, +} from "../utils/lua-ast"; +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 { isAsyncFunction, wrapInAsyncAwaiter } from "./async-await"; +import { transformIdentifier } from "./identifier"; +import { transformExpressionBodyToReturnStatement } from "./return"; +import { transformBindingPattern } from "./variable-declaration"; + +function transformParameterDefaultValueDeclaration( + context: TransformationContext, + parameterName: lua.Identifier, + value: ts.Expression, + tsOriginal?: ts.Node +): lua.Statement | undefined { + const { precedingStatements: statements, result: parameterValue } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(value) + ); + if (!lua.isNilLiteral(parameterValue)) { + statements.push(lua.createAssignmentStatement(parameterName, parameterValue)); + } + if (statements.length === 0) return undefined; + + const nilCondition = lua.createBinaryExpression( + parameterName, + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator + ); + + const ifBlock = lua.createBlock(statements, tsOriginal); + + return lua.createIfStatement(nilCondition, ifBlock, undefined, tsOriginal); +} + +function isRestParameterReferenced(identifier: lua.Identifier, scope: Scope): boolean { + if (!identifier.symbolId) { + return true; + } + if (scope.referencedSymbols === undefined) { + return false; + } + const references = scope.referencedSymbols.get(identifier.symbolId); + return references !== undefined && references.length > 0; +} + +export function createCallableTable(functionExpression: lua.Expression): lua.Expression { + // __call metamethod receives the table as the first argument, so we need to add a dummy parameter + if (lua.isFunctionExpression(functionExpression)) { + functionExpression.params?.unshift(lua.createAnonymousIdentifier()); + } else { + // functionExpression may have been replaced (lib functions, etc...), + // so we create a forwarding function to eat the extra argument + functionExpression = lua.createFunctionExpression( + lua.createBlock([ + lua.createReturnStatement([lua.createCallExpression(functionExpression, [lua.createDotsLiteral()])]), + ]), + [lua.createAnonymousIdentifier()], + lua.createDotsLiteral(), + lua.NodeFlags.Inline + ); + } + return lua.createCallExpression(lua.createIdentifier("setmetatable"), [ + lua.createTableExpression(), + lua.createTableExpression([ + lua.createTableFieldExpression(functionExpression, lua.createStringLiteral("__call")), + ]), + ]); +} + +export function isFunctionTypeWithProperties(context: TransformationContext, functionType: ts.Type): boolean { + if (functionType.isUnion()) { + return functionType.types.some(t => isFunctionTypeWithProperties(context, t)); + } else { + return ( + isFunctionType(functionType) && + functionType.getProperties().length > 0 && + getExtensionKindForType(context, functionType) === undefined // ignore TSTL extension functions like $range + ); + } +} + +export function transformFunctionBodyContent(context: TransformationContext, body: ts.ConciseBody): lua.Statement[] { + if (!ts.isBlock(body)) { + const { precedingStatements, result: returnStatement } = transformInPrecedingStatementScope(context, () => + transformExpressionBodyToReturnStatement(context, body) + ); + return [...precedingStatements, returnStatement]; + } + + const bodyStatements = performHoisting(context, context.transformStatements(body.statements)); + return bodyStatements; +} + +export function transformFunctionBodyHeader( + context: TransformationContext, + bodyScope: Scope, + parameters: ts.NodeArray, + spreadIdentifier?: lua.Identifier +): lua.Statement[] { + const headerStatements: lua.Statement[] = []; + + // Add default parameters and object binding patterns + const bindingPatternDeclarations: lua.Statement[] = []; + let bindPatternIndex = 0; + for (const declaration of parameters) { + if (ts.isObjectBindingPattern(declaration.name) || ts.isArrayBindingPattern(declaration.name)) { + const identifier = lua.createIdentifier(`____bindingPattern${bindPatternIndex++}`); + if (declaration.initializer !== undefined) { + // Default binding parameter + const initializer = transformParameterDefaultValueDeclaration( + context, + identifier, + declaration.initializer + ); + if (initializer) headerStatements.push(initializer); + } + + // Binding pattern + const name = declaration.name; + const { precedingStatements, result: bindings } = transformInPrecedingStatementScope(context, () => + transformBindingPattern(context, name, identifier) + ); + bindingPatternDeclarations.push(...precedingStatements, ...bindings); + } else if (declaration.initializer !== undefined) { + // Default parameter + const initializer = transformParameterDefaultValueDeclaration( + context, + transformIdentifier(context, declaration.name), + declaration.initializer + ); + if (initializer) headerStatements.push(initializer); + } + } + + // Push spread operator here + if (spreadIdentifier && isRestParameterReferenced(spreadIdentifier, bodyScope)) { + const spreadTable = + context.luaTarget === LuaTarget.Lua50 ? lua.createArgLiteral() : wrapInTable(lua.createDotsLiteral()); + headerStatements.push(lua.createVariableDeclarationStatement(spreadIdentifier, spreadTable)); + } + + // Binding pattern statements need to be after spread table is declared + headerStatements.push(...bindingPatternDeclarations); + + return headerStatements; +} + +export function transformFunctionBody( + context: TransformationContext, + parameters: ts.NodeArray, + body: ts.ConciseBody, + node: ts.FunctionLikeDeclaration, + spreadIdentifier?: lua.Identifier +): [lua.Statement[], Scope] { + const scope = context.pushScope(ScopeType.Function, node); + let bodyStatements = transformFunctionBodyContent(context, body); + if (node && isAsyncFunction(node)) { + bodyStatements = [lua.createReturnStatement([wrapInAsyncAwaiter(context, bodyStatements)])]; + } + const headerStatements = transformFunctionBodyHeader(context, scope, parameters, spreadIdentifier); + context.popScope(); + return [[...headerStatements, ...bodyStatements], scope]; +} + +export function transformParameters( + context: TransformationContext, + parameters: ts.NodeArray, + functionContext?: lua.Identifier +): [lua.Identifier[], lua.DotsLiteral | undefined, lua.Identifier | undefined] { + // Build parameter string + const paramNames: lua.Identifier[] = []; + if (functionContext) { + paramNames.push(functionContext); + } + + let restParamName: lua.Identifier | undefined; + let dotsLiteral: lua.DotsLiteral | undefined; + let identifierIndex = 0; + + // Only push parameter name to paramName array if it isn't a spread parameter + for (const param of parameters) { + if (ts.isIdentifier(param.name) && ts.identifierToKeywordKind(param.name) === ts.SyntaxKind.ThisKeyword) { + continue; + } + + // Binding patterns become ____bindingPattern0, ____bindingPattern1, etc as function parameters + // See transformFunctionBody for how these values are destructured + const paramName = + ts.isObjectBindingPattern(param.name) || ts.isArrayBindingPattern(param.name) + ? lua.createIdentifier(`____bindingPattern${identifierIndex++}`) + : transformIdentifier(context, param.name); + + // This parameter is a spread parameter (...param) + if (!param.dotDotDotToken) { + paramNames.push(paramName); + } else { + restParamName = paramName; + // Push the spread operator into the paramNames array + dotsLiteral = lua.createDotsLiteral(); + } + } + + return [paramNames, dotsLiteral, restParamName]; +} + +export function transformFunctionToExpression( + context: TransformationContext, + node: ts.FunctionLikeDeclaration +): [lua.Expression, Scope] { + assert(node.body); + + const type = context.checker.getTypeAtLocation(node); + let functionContext: lua.Identifier | undefined; + + const firstParam = node.parameters[0]; + const hasThisVoidParameter = + firstParam && + ts.isIdentifier(firstParam.name) && + ts.identifierToKeywordKind(firstParam.name) === ts.SyntaxKind.ThisKeyword && + firstParam.type?.kind === ts.SyntaxKind.VoidKeyword; + + if (!hasThisVoidParameter && getFunctionContextType(context, type) !== ContextType.Void) { + if (ts.isArrowFunction(node)) { + // dummy context for arrow functions with parameters + if (node.parameters.length > 0) { + functionContext = lua.createAnonymousIdentifier(); + } + } else { + // self context + functionContext = createSelfIdentifier(); + } + } + + let flags = lua.NodeFlags.None; + if (!ts.isBlock(node.body)) flags |= lua.NodeFlags.Inline; + if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) { + flags |= lua.NodeFlags.Declaration; + } + + const [paramNames, dotsLiteral, spreadIdentifier] = transformParameters(context, node.parameters, functionContext); + const [transformedBody, functionScope] = transformFunctionBody( + context, + node.parameters, + node.body, + node, + spreadIdentifier + ); + + const functionExpression = lua.createFunctionExpression( + lua.createBlock(transformedBody), + paramNames, + dotsLiteral, + flags, + node + ); + + return [ + node.asteriskToken + ? transformLuaLibFunction(context, LuaLibFeature.Generator, undefined, functionExpression) + : functionExpression, + functionScope, + ]; +} + +export function transformFunctionLikeDeclaration( + node: ts.FunctionLikeDeclaration, + context: TransformationContext +): lua.Expression { + if (node.body === undefined) { + // This code can be reached only from object methods, which is TypeScript error + return lua.createNilLiteral(); + } + + const [functionExpression, functionScope] = transformFunctionToExpression(context, node); + + const isNamedFunctionExpression = ts.isFunctionExpression(node) && node.name; + // Handle named function expressions which reference themselves + if (isNamedFunctionExpression && functionScope.referencedSymbols) { + const symbol = context.checker.getSymbolAtLocation(node.name); + if (symbol) { + // TODO: Not using symbol ids because of https://github.com/microsoft/TypeScript/issues/37131 + const isReferenced = [...functionScope.referencedSymbols].some(([, nodes]) => + nodes.some(n => context.checker.getSymbolAtLocation(n)?.valueDeclaration === symbol.valueDeclaration) + ); + + // Only handle if the name is actually referenced inside the function + if (isReferenced) { + const nameIdentifier = transformIdentifier(context, node.name); + if (isFunctionTypeWithProperties(context, context.checker.getTypeAtLocation(node))) { + context.addPrecedingStatements([ + lua.createVariableDeclarationStatement(nameIdentifier), + lua.createAssignmentStatement(nameIdentifier, createCallableTable(functionExpression)), + ]); + } else { + context.addPrecedingStatements( + lua.createVariableDeclarationStatement(nameIdentifier, functionExpression) + ); + } + return lua.cloneIdentifier(nameIdentifier); + } + } + } + + return isNamedFunctionExpression && isFunctionTypeWithProperties(context, context.checker.getTypeAtLocation(node)) + ? createCallableTable(functionExpression) + : functionExpression; +} + +export const transformFunctionDeclaration: FunctionVisitor = (node, context) => { + // Don't transform functions without body (overload declarations) + if (node.body === undefined) { + return undefined; + } + + if (hasDefaultExportModifier(node)) { + return lua.createAssignmentStatement( + lua.createTableIndexExpression(createExportsIdentifier(), createDefaultExportStringLiteral(node)), + transformFunctionLikeDeclaration(node, context) + ); + } + + const [functionExpression, functionScope] = transformFunctionToExpression(context, node); + + // Name being undefined without default export is a TypeScript error + const name = node.name ? transformIdentifier(context, node.name) : lua.createAnonymousIdentifier(); + + // Remember symbols referenced in this function for hoisting later + if (name.symbolId !== undefined) { + const scope = peekScope(context); + scope.functionDefinitions ??= new Map(); + + const functionInfo = { referencedSymbols: functionScope.referencedSymbols ?? new Map() }; + scope.functionDefinitions.set(name.symbolId, functionInfo); + } + + // Wrap functions with properties into a callable table + const wrappedFunction = + node.name && isFunctionTypeWithProperties(context, context.checker.getTypeAtLocation(node.name)) + ? createCallableTable(functionExpression) + : functionExpression; + + return createLocalOrExportedOrGlobalDeclaration(context, name, wrappedFunction, node); +}; + +export const transformYieldExpression: FunctionVisitor = (expression, context) => { + const parameters = expression.expression ? [context.transformExpression(expression.expression)] : []; + return expression.asteriskToken + ? transformLuaLibFunction(context, LuaLibFeature.DelegatedYield, expression, ...parameters) + : lua.createCallExpression( + lua.createTableIndexExpression(lua.createIdentifier("coroutine"), lua.createStringLiteral("yield")), + parameters, + expression + ); +}; diff --git a/src/transformation/visitors/identifier.ts b/src/transformation/visitors/identifier.ts new file mode 100644 index 000000000..3b2e016e4 --- /dev/null +++ b/src/transformation/visitors/identifier.ts @@ -0,0 +1,134 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { transformBuiltinIdentifierExpression, checkForLuaLibType } from "../builtins"; +import { isPromiseClass, createPromiseIdentifier } from "../builtins/promise"; +import { FunctionVisitor, tempSymbolId, TransformationContext } from "../context"; +import { invalidCallExtensionUse } from "../utils/diagnostics"; +import { createExportedIdentifier, getSymbolExportScope } from "../utils/export"; +import { createSafeName, hasUnsafeIdentifierName } from "../utils/safe-names"; +import { getIdentifierSymbolId } from "../utils/symbols"; +import { getOptionalContinuationData, isOptionalContinuation } from "./optional-chaining"; +import { isStandardLibraryType } from "../utils/typescript"; +import { getExtensionKindForNode, getExtensionKindForSymbol } from "../utils/language-extensions"; +import { callExtensions } from "./language-extensions/call-extension"; +import { isIdentifierExtensionValue, reportInvalidExtensionValue } from "./language-extensions/identifier"; +import { Annotation, AnnotationKind, getNodeAnnotations } from "../utils/annotations"; + +export function transformIdentifier(context: TransformationContext, identifier: ts.Identifier): lua.Identifier { + return transformNonValueIdentifier(context, identifier, context.checker.getSymbolAtLocation(identifier)); +} + +export function getCustomNameFromSymbol(context: TransformationContext, symbol?: ts.Symbol): undefined | string { + let retVal: undefined | string; + + if (symbol) { + const declarations = symbol.getDeclarations(); + if (declarations) { + let customNameAnnotation: undefined | Annotation = undefined; + for (const declaration of declarations) { + const nodeAnnotations = getNodeAnnotations(declaration); + const foundAnnotation = nodeAnnotations.get(AnnotationKind.CustomName); + + if (foundAnnotation) { + customNameAnnotation = foundAnnotation; + break; + } + + // If the symbol is an imported value, check the original declaration + // beware of declaration.propertyName, this is the import name alias and should not be renamed! + if (ts.isImportSpecifier(declaration) && !declaration.propertyName) { + const importedType = context.checker.getTypeAtLocation(declaration); + if (importedType.symbol) { + const importedCustomName = getCustomNameFromSymbol(context, importedType.symbol); + if (importedCustomName) { + return importedCustomName; + } + } + } + } + + if (customNameAnnotation) { + retVal = customNameAnnotation.args[0]; + } + } + } + + return retVal; +} + +function transformNonValueIdentifier( + context: TransformationContext, + identifier: ts.Identifier, + symbol: ts.Symbol | undefined +) { + if (isOptionalContinuation(identifier)) { + const result = lua.createIdentifier(identifier.text, undefined, tempSymbolId); + getOptionalContinuationData(identifier)!.usedIdentifiers.push(result); + return result; + } + + const extensionKind = symbol + ? getExtensionKindForSymbol(context, symbol) + : getExtensionKindForNode(context, identifier); + + if (extensionKind) { + if (callExtensions.has(extensionKind)) { + // Avoid putting duplicate diagnostic on the name of a variable declaration, due to the inferred type + if (!(ts.isVariableDeclaration(identifier.parent) && identifier.parent.name === identifier)) { + context.diagnostics.push(invalidCallExtensionUse(identifier)); + } + // fall through + } else if (isIdentifierExtensionValue(symbol, extensionKind)) { + reportInvalidExtensionValue(context, identifier, extensionKind); + return lua.createAnonymousIdentifier(identifier); + } + } + + const type = context.checker.getTypeAtLocation(identifier); + if (isStandardLibraryType(context, type, undefined)) { + checkForLuaLibType(context, type); + if (isPromiseClass(context, identifier)) { + return createPromiseIdentifier(identifier); + } + } + + let text = hasUnsafeIdentifierName(context, identifier, symbol) ? createSafeName(identifier.text) : identifier.text; + + const customName = getCustomNameFromSymbol(context, symbol); + if (customName) text = customName; + + const symbolId = getIdentifierSymbolId(context, identifier, symbol); + return lua.createIdentifier(text, identifier, symbolId, identifier.text); +} + +export function transformIdentifierWithSymbol( + context: TransformationContext, + node: ts.Identifier, + symbol: ts.Symbol | undefined +): lua.Expression { + if (symbol) { + const exportScope = getSymbolExportScope(context, symbol); + if (exportScope) { + const name = symbol.name; + const text = hasUnsafeIdentifierName(context, node, symbol) ? createSafeName(name) : name; + const symbolId = getIdentifierSymbolId(context, node, symbol); + const identifier = lua.createIdentifier(text, node, symbolId, name); + return createExportedIdentifier(context, identifier, exportScope); + } + } + const builtinResult = transformBuiltinIdentifierExpression(context, node, symbol); + if (builtinResult) { + return builtinResult; + } + + return transformNonValueIdentifier(context, node, symbol); +} + +export const transformIdentifierExpression: FunctionVisitor = (node, context) => { + if (ts.identifierToKeywordKind(node) === ts.SyntaxKind.UndefinedKeyword) { + return lua.createNilLiteral(node); + } + + const symbol = context.checker.getSymbolAtLocation(node); + return transformIdentifierWithSymbol(context, node, symbol); +}; diff --git a/src/transformation/visitors/index.ts b/src/transformation/visitors/index.ts new file mode 100644 index 000000000..466f5ed35 --- /dev/null +++ b/src/transformation/visitors/index.ts @@ -0,0 +1,103 @@ +import * as ts from "typescript"; +import { FunctionVisitor, Visitors } from "../context"; +import { transformElementAccessExpression, transformPropertyAccessExpression, transformQualifiedName } from "./access"; +import { transformBinaryExpression } from "./binary-expression"; +import { transformBlock } from "./block"; +import { transformBreakStatement, transformContinueStatement } from "./break-continue"; +import { transformCallExpression } from "./call"; +import { transformSpreadElement } from "./spread"; +import { + transformClassAsExpression, + transformClassDeclaration, + transformSuperExpression, + transformThisExpression, +} from "./class"; +import { transformNewExpression } from "./class/new"; +import { transformConditionalExpression, transformIfStatement } from "./conditional"; +import { transformDeleteExpression } from "./delete"; +import { transformEnumDeclaration } from "./enum"; +import { transformThrowStatement, transformTryStatement } from "./errors"; +import { transformExpressionStatement } from "./expression-statement"; +import { transformFunctionDeclaration, transformFunctionLikeDeclaration, transformYieldExpression } from "./function"; +import { transformIdentifierExpression } from "./identifier"; +import { literalVisitors } from "./literal"; +import { transformDoStatement, transformWhileStatement } from "./loops/do-while"; +import { transformForStatement } from "./loops/for"; +import { transformForInStatement } from "./loops/for-in"; +import { transformForOfStatement } from "./loops/for-of"; +import { transformExportAssignment, transformExportDeclaration } from "./modules/export"; +import { + transformExternalModuleReference, + transformImportDeclaration, + transformImportEqualsDeclaration, +} from "./modules/import"; +import { transformModuleDeclaration } from "./namespace"; +import { transformReturnStatement } from "./return"; +import { transformSourceFileNode } from "./sourceFile"; +import { transformSwitchStatement } from "./switch"; +import { transformTaggedTemplateExpression, transformTemplateExpression } from "./template"; +import { transformTypeOfExpression } from "./typeof"; +import { typescriptVisitors } from "./typescript"; +import { transformPostfixUnaryExpression, transformPrefixUnaryExpression } from "./unary-expression"; +import { transformVariableStatement } from "./variable-declaration"; +import { transformAwaitExpression } from "./async-await"; +import { transformVoidExpression } from "./void"; + +const transformEmptyStatement: FunctionVisitor = () => undefined; +const transformParenthesizedExpression: FunctionVisitor = (node, context) => + context.transformExpression(node.expression); + +export const standardVisitors: Visitors = { + ...literalVisitors, + ...typescriptVisitors, + [ts.SyntaxKind.ArrowFunction]: transformFunctionLikeDeclaration, + [ts.SyntaxKind.AwaitExpression]: transformAwaitExpression, + [ts.SyntaxKind.BinaryExpression]: transformBinaryExpression, + [ts.SyntaxKind.Block]: transformBlock, + [ts.SyntaxKind.BreakStatement]: transformBreakStatement, + [ts.SyntaxKind.CallExpression]: transformCallExpression, + [ts.SyntaxKind.ClassDeclaration]: transformClassDeclaration, + [ts.SyntaxKind.ClassExpression]: transformClassAsExpression, + [ts.SyntaxKind.ConditionalExpression]: transformConditionalExpression, + [ts.SyntaxKind.ContinueStatement]: transformContinueStatement, + [ts.SyntaxKind.DeleteExpression]: transformDeleteExpression, + [ts.SyntaxKind.DoStatement]: transformDoStatement, + [ts.SyntaxKind.ElementAccessExpression]: transformElementAccessExpression, + [ts.SyntaxKind.EmptyStatement]: transformEmptyStatement, + [ts.SyntaxKind.EnumDeclaration]: transformEnumDeclaration, + [ts.SyntaxKind.ExportAssignment]: transformExportAssignment, + [ts.SyntaxKind.ExportDeclaration]: transformExportDeclaration, + [ts.SyntaxKind.ExpressionStatement]: transformExpressionStatement, + [ts.SyntaxKind.ExternalModuleReference]: transformExternalModuleReference, + [ts.SyntaxKind.ForInStatement]: transformForInStatement, + [ts.SyntaxKind.ForOfStatement]: transformForOfStatement, + [ts.SyntaxKind.ForStatement]: transformForStatement, + [ts.SyntaxKind.FunctionDeclaration]: transformFunctionDeclaration, + [ts.SyntaxKind.FunctionExpression]: transformFunctionLikeDeclaration, + [ts.SyntaxKind.Identifier]: transformIdentifierExpression, + [ts.SyntaxKind.IfStatement]: transformIfStatement, + [ts.SyntaxKind.ImportDeclaration]: transformImportDeclaration, + [ts.SyntaxKind.ImportEqualsDeclaration]: transformImportEqualsDeclaration, + [ts.SyntaxKind.ModuleDeclaration]: transformModuleDeclaration, + [ts.SyntaxKind.NewExpression]: transformNewExpression, + [ts.SyntaxKind.ParenthesizedExpression]: transformParenthesizedExpression, + [ts.SyntaxKind.PostfixUnaryExpression]: transformPostfixUnaryExpression, + [ts.SyntaxKind.PrefixUnaryExpression]: transformPrefixUnaryExpression, + [ts.SyntaxKind.PropertyAccessExpression]: transformPropertyAccessExpression, + [ts.SyntaxKind.QualifiedName]: transformQualifiedName, + [ts.SyntaxKind.ReturnStatement]: transformReturnStatement, + [ts.SyntaxKind.SourceFile]: transformSourceFileNode, + [ts.SyntaxKind.SpreadElement]: transformSpreadElement, + [ts.SyntaxKind.SuperKeyword]: transformSuperExpression, + [ts.SyntaxKind.SwitchStatement]: transformSwitchStatement, + [ts.SyntaxKind.TaggedTemplateExpression]: transformTaggedTemplateExpression, + [ts.SyntaxKind.TemplateExpression]: transformTemplateExpression, + [ts.SyntaxKind.ThisKeyword]: transformThisExpression, + [ts.SyntaxKind.ThrowStatement]: transformThrowStatement, + [ts.SyntaxKind.TryStatement]: transformTryStatement, + [ts.SyntaxKind.TypeOfExpression]: transformTypeOfExpression, + [ts.SyntaxKind.VariableStatement]: transformVariableStatement, + [ts.SyntaxKind.WhileStatement]: transformWhileStatement, + [ts.SyntaxKind.YieldExpression]: transformYieldExpression, + [ts.SyntaxKind.VoidExpression]: transformVoidExpression, +}; diff --git a/src/transformation/visitors/language-extensions/call-extension.ts b/src/transformation/visitors/language-extensions/call-extension.ts new file mode 100644 index 000000000..49c05b72e --- /dev/null +++ b/src/transformation/visitors/language-extensions/call-extension.ts @@ -0,0 +1,34 @@ +import { TransformationContext } from "../../context"; +import * as ts from "typescript"; +import { ExtensionKind, getExtensionKindForNode } from "../../utils/language-extensions"; +import * as lua from "../../../LuaAST"; +import { operatorExtensionTransformers } from "./operators"; +import { tableExtensionTransformers, tableNewExtensions } from "./table"; + +const allCallExtensionHandlers: LanguageExtensionCallTransformerMap = { + ...operatorExtensionTransformers, + ...tableExtensionTransformers, +}; +export const callExtensions = new Set(Object.keys(allCallExtensionHandlers) as ExtensionKind[]); +tableNewExtensions.forEach(kind => callExtensions.add(kind)); + +export type LanguageExtensionCallTransformer = ( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +) => lua.Expression; + +export type LanguageExtensionCallTransformerMap = { + [P in ExtensionKind]?: LanguageExtensionCallTransformer; +}; +export function transformLanguageExtensionCallExpression( + context: TransformationContext, + node: ts.CallExpression +): lua.Expression | undefined { + const extensionKind = getExtensionKindForNode(context, node.expression); + if (!extensionKind) return; + const transformer = allCallExtensionHandlers[extensionKind]; + if (transformer) { + return transformer(context, node, extensionKind); + } +} diff --git a/src/transformation/visitors/language-extensions/identifier.ts b/src/transformation/visitors/language-extensions/identifier.ts new file mode 100644 index 000000000..945126e6d --- /dev/null +++ b/src/transformation/visitors/language-extensions/identifier.ts @@ -0,0 +1,27 @@ +import * as ts from "typescript"; +import { ExtensionKind } from "../../utils/language-extensions"; +import { TransformationContext } from "../../context"; +import { invalidMultiFunctionUse, invalidRangeUse, invalidVarargUse } from "../../utils/diagnostics"; + +const extensionKindToValueName: { [T in ExtensionKind]?: string } = { + [ExtensionKind.MultiFunction]: "$multi", + [ExtensionKind.RangeFunction]: "$range", + [ExtensionKind.VarargConstant]: "$vararg", +}; +export function isIdentifierExtensionValue(symbol: ts.Symbol | undefined, extensionKind: ExtensionKind): boolean { + return symbol !== undefined && extensionKindToValueName[extensionKind] === symbol.name; +} + +export function reportInvalidExtensionValue( + context: TransformationContext, + identifier: ts.Identifier, + extensionKind: ExtensionKind +): void { + if (extensionKind === ExtensionKind.MultiFunction) { + context.diagnostics.push(invalidMultiFunctionUse(identifier)); + } else if (extensionKind === ExtensionKind.RangeFunction) { + context.diagnostics.push(invalidRangeUse(identifier)); + } else if (extensionKind === ExtensionKind.VarargConstant) { + context.diagnostics.push(invalidVarargUse(identifier)); + } +} diff --git a/src/transformation/visitors/language-extensions/iterable.ts b/src/transformation/visitors/language-extensions/iterable.ts new file mode 100644 index 000000000..ee1ded7be --- /dev/null +++ b/src/transformation/visitors/language-extensions/iterable.ts @@ -0,0 +1,114 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { TransformationContext } from "../../context"; +import { getVariableDeclarationBinding, transformForInitializer } from "../loops/utils"; +import { transformArrayBindingElement } from "../variable-declaration"; +import { + invalidMultiIterableWithoutDestructuring, + invalidPairsIterableWithoutDestructuring, +} from "../../utils/diagnostics"; +import { cast } from "../../../utils"; +import { isMultiReturnType } from "./multi"; + +function transformForOfMultiIterableStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block, + luaIterator: lua.Expression, + invalidMultiUseDiagnostic: (node: ts.Node) => ts.Diagnostic +): lua.Statement { + context.pushPrecedingStatements(); + let identifiers: lua.Identifier[] = []; + + if (ts.isVariableDeclarationList(statement.initializer)) { + // Variables declared in for loop + // for ${initializer} in ${iterable} do + const binding = getVariableDeclarationBinding(context, statement.initializer); + if (ts.isArrayBindingPattern(binding)) { + identifiers = binding.elements.map(e => transformArrayBindingElement(context, e)); + } else { + context.diagnostics.push(invalidMultiUseDiagnostic(binding)); + } + } else if (ts.isArrayLiteralExpression(statement.initializer)) { + // Variables NOT declared in for loop - catch iterator values in temps and assign + // for ____value0 in ${iterable} do + // ${initializer} = ____value0 + identifiers = statement.initializer.elements.map((_, i) => lua.createIdentifier(`____value${i}`)); + if (identifiers.length > 0) { + block.statements.unshift( + lua.createAssignmentStatement( + statement.initializer.elements.map(e => + cast(context.transformExpression(e), lua.isAssignmentLeftHandSideExpression) + ), + identifiers + ) + ); + } + } else { + context.diagnostics.push(invalidMultiUseDiagnostic(statement.initializer)); + } + + if (identifiers.length === 0) { + identifiers.push(lua.createAnonymousIdentifier()); + } + + block.statements.unshift(...context.popPrecedingStatements()); + + return lua.createForInStatement(block, identifiers, [luaIterator], statement); +} + +export function transformForOfIterableStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block +): lua.Statement { + const iteratedExpressionType = context.checker.getTypeAtLocation(statement.expression); + const iterableType = + iteratedExpressionType.isIntersection() && + iteratedExpressionType.types.find(t => t.symbol.escapedName === "Iterable"); + const iterableTypeArguments = (iterableType as ts.TypeReference)?.typeArguments; + + if (iterableTypeArguments && iterableTypeArguments.length > 0 && isMultiReturnType(iterableTypeArguments[0])) { + const luaIterator = context.transformExpression(statement.expression); + return transformForOfMultiIterableStatement( + context, + statement, + block, + luaIterator, + invalidMultiIterableWithoutDestructuring + ); + } + + const luaIterator = context.transformExpression(statement.expression); + const identifier = transformForInitializer(context, statement.initializer, block); + return lua.createForInStatement(block, [identifier], [luaIterator], statement); +} + +export function transformForOfPairsIterableStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block +): lua.Statement { + const pairsCall = lua.createCallExpression(lua.createIdentifier("pairs"), [ + context.transformExpression(statement.expression), + ]); + return transformForOfMultiIterableStatement( + context, + statement, + block, + pairsCall, + invalidPairsIterableWithoutDestructuring + ); +} + +export function transformForOfPairsKeyIterableStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block +): lua.Statement { + const pairsCall = lua.createCallExpression(lua.createIdentifier("pairs"), [ + context.transformExpression(statement.expression), + ]); + const identifier = transformForInitializer(context, statement.initializer, block); + return lua.createForInStatement(block, [identifier], [pairsCall], statement); +} diff --git a/src/transformation/visitors/language-extensions/multi.ts b/src/transformation/visitors/language-extensions/multi.ts new file mode 100644 index 000000000..d9d7cbc02 --- /dev/null +++ b/src/transformation/visitors/language-extensions/multi.ts @@ -0,0 +1,109 @@ +import * as ts from "typescript"; +import * as extensions from "../../utils/language-extensions"; +import { + getExtensionKindForNode, + getIterableExtensionKindForNode, + IterableExtensionKind, +} from "../../utils/language-extensions"; +import { TransformationContext } from "../../context"; +import { findFirstNodeAbove, findFirstNonOuterParent } from "../../utils/typescript"; + +const multiReturnExtensionName = "__tstlMultiReturn"; +export function isMultiReturnType(type: ts.Type): boolean { + return type.getProperty(multiReturnExtensionName) !== undefined; +} + +export function canBeMultiReturnType(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.Any) !== 0 || + isMultiReturnType(type) || + (type.isUnion() && type.types.some(t => canBeMultiReturnType(t))) + ); +} + +export function isMultiFunctionCall(context: TransformationContext, expression: ts.CallExpression): boolean { + return isMultiFunctionNode(context, expression.expression); +} + +export function returnsMultiType(context: TransformationContext, node: ts.CallExpression): boolean { + const signature = context.checker.getResolvedSignature(node); + const type = signature?.getReturnType(); + return type ? isMultiReturnType(type) : false; +} + +export function isMultiReturnCall(context: TransformationContext, expression: ts.Expression) { + return ts.isCallExpression(expression) && returnsMultiType(context, expression); +} + +export function isMultiFunctionNode(context: TransformationContext, node: ts.Node): boolean { + return ( + ts.isIdentifier(node) && + node.text === "$multi" && + getExtensionKindForNode(context, node) === extensions.ExtensionKind.MultiFunction + ); +} + +export function isInMultiReturnFunction(context: TransformationContext, node: ts.Node) { + const declaration = findFirstNodeAbove(node, ts.isFunctionLike); + if (!declaration) { + return false; + } + const signature = context.checker.getSignatureFromDeclaration(declaration); + const type = signature?.getReturnType(); + return type ? isMultiReturnType(type) : false; +} + +export function shouldMultiReturnCallBeWrapped(context: TransformationContext, node: ts.CallExpression) { + if (!returnsMultiType(context, node)) { + return false; + } + + const parent = findFirstNonOuterParent(node); + + // Variable declaration with destructuring + if (ts.isVariableDeclaration(parent) && ts.isArrayBindingPattern(parent.name)) { + return false; + } + + // Variable assignment with destructuring + if ( + ts.isBinaryExpression(parent) && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isArrayLiteralExpression(parent.left) + ) { + return false; + } + + // Spread operator + if (ts.isSpreadElement(parent)) { + return false; + } + + // Stand-alone expression + if (ts.isExpressionStatement(parent)) { + return false; + } + + // Forwarded multi-return call + if ( + (ts.isReturnStatement(parent) || ts.isArrowFunction(parent)) && // Body-less arrow func + isInMultiReturnFunction(context, node) + ) { + return false; + } + + // Element access expression 'foo()[0]' will be optimized using 'select' + if (ts.isElementAccessExpression(parent)) { + return false; + } + + // LuaIterable in for...of + if ( + ts.isForOfStatement(parent) && + getIterableExtensionKindForNode(context, node) === IterableExtensionKind.Iterable + ) { + return false; + } + + return true; +} diff --git a/src/transformation/visitors/language-extensions/operators.ts b/src/transformation/visitors/language-extensions/operators.ts new file mode 100644 index 000000000..4cb08abc2 --- /dev/null +++ b/src/transformation/visitors/language-extensions/operators.ts @@ -0,0 +1,125 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { TransformationContext } from "../../context"; +import { assert } from "../../../utils"; +import { LuaTarget } from "../../../CompilerOptions"; +import { unsupportedForTarget } from "../../utils/diagnostics"; +import { ExtensionKind, getBinaryCallExtensionArgs, getUnaryCallExtensionArg } from "../../utils/language-extensions"; +import { LanguageExtensionCallTransformerMap } from "./call-extension"; +import { transformOrderedExpressions } from "../expression-list"; + +const binaryOperatorMappings = new Map([ + [ExtensionKind.AdditionOperatorType, lua.SyntaxKind.AdditionOperator], + [ExtensionKind.AdditionOperatorMethodType, lua.SyntaxKind.AdditionOperator], + [ExtensionKind.SubtractionOperatorType, lua.SyntaxKind.SubtractionOperator], + [ExtensionKind.SubtractionOperatorMethodType, lua.SyntaxKind.SubtractionOperator], + [ExtensionKind.MultiplicationOperatorType, lua.SyntaxKind.MultiplicationOperator], + [ExtensionKind.MultiplicationOperatorMethodType, lua.SyntaxKind.MultiplicationOperator], + [ExtensionKind.DivisionOperatorType, lua.SyntaxKind.DivisionOperator], + [ExtensionKind.DivisionOperatorMethodType, lua.SyntaxKind.DivisionOperator], + [ExtensionKind.ModuloOperatorType, lua.SyntaxKind.ModuloOperator], + [ExtensionKind.ModuloOperatorMethodType, lua.SyntaxKind.ModuloOperator], + [ExtensionKind.PowerOperatorType, lua.SyntaxKind.PowerOperator], + [ExtensionKind.PowerOperatorMethodType, lua.SyntaxKind.PowerOperator], + [ExtensionKind.FloorDivisionOperatorType, lua.SyntaxKind.FloorDivisionOperator], + [ExtensionKind.FloorDivisionOperatorMethodType, lua.SyntaxKind.FloorDivisionOperator], + [ExtensionKind.BitwiseAndOperatorType, lua.SyntaxKind.BitwiseAndOperator], + [ExtensionKind.BitwiseAndOperatorMethodType, lua.SyntaxKind.BitwiseAndOperator], + [ExtensionKind.BitwiseOrOperatorType, lua.SyntaxKind.BitwiseOrOperator], + [ExtensionKind.BitwiseOrOperatorMethodType, lua.SyntaxKind.BitwiseOrOperator], + [ExtensionKind.BitwiseExclusiveOrOperatorType, lua.SyntaxKind.BitwiseExclusiveOrOperator], + [ExtensionKind.BitwiseExclusiveOrOperatorMethodType, lua.SyntaxKind.BitwiseExclusiveOrOperator], + [ExtensionKind.BitwiseLeftShiftOperatorType, lua.SyntaxKind.BitwiseLeftShiftOperator], + [ExtensionKind.BitwiseLeftShiftOperatorMethodType, lua.SyntaxKind.BitwiseLeftShiftOperator], + [ExtensionKind.BitwiseRightShiftOperatorType, lua.SyntaxKind.BitwiseRightShiftOperator], + [ExtensionKind.BitwiseRightShiftOperatorMethodType, lua.SyntaxKind.BitwiseRightShiftOperator], + [ExtensionKind.ConcatOperatorType, lua.SyntaxKind.ConcatOperator], + [ExtensionKind.ConcatOperatorMethodType, lua.SyntaxKind.ConcatOperator], + [ExtensionKind.LessThanOperatorType, lua.SyntaxKind.LessThanOperator], + [ExtensionKind.LessThanOperatorMethodType, lua.SyntaxKind.LessThanOperator], + [ExtensionKind.GreaterThanOperatorType, lua.SyntaxKind.GreaterThanOperator], + [ExtensionKind.GreaterThanOperatorMethodType, lua.SyntaxKind.GreaterThanOperator], +]); + +const unaryOperatorMappings = new Map([ + [ExtensionKind.NegationOperatorType, lua.SyntaxKind.NegationOperator], + [ExtensionKind.NegationOperatorMethodType, lua.SyntaxKind.NegationOperator], + [ExtensionKind.BitwiseNotOperatorType, lua.SyntaxKind.BitwiseNotOperator], + [ExtensionKind.BitwiseNotOperatorMethodType, lua.SyntaxKind.BitwiseNotOperator], + [ExtensionKind.LengthOperatorType, lua.SyntaxKind.LengthOperator], + [ExtensionKind.LengthOperatorMethodType, lua.SyntaxKind.LengthOperator], +]); + +const bitwiseOperatorMapExtensions = new Set([ + ExtensionKind.BitwiseAndOperatorType, + ExtensionKind.BitwiseAndOperatorMethodType, + ExtensionKind.BitwiseOrOperatorType, + ExtensionKind.BitwiseOrOperatorMethodType, + ExtensionKind.BitwiseExclusiveOrOperatorType, + ExtensionKind.BitwiseExclusiveOrOperatorMethodType, + ExtensionKind.BitwiseLeftShiftOperatorType, + ExtensionKind.BitwiseLeftShiftOperatorMethodType, + ExtensionKind.BitwiseRightShiftOperatorType, + ExtensionKind.BitwiseRightShiftOperatorMethodType, + ExtensionKind.BitwiseNotOperatorType, + ExtensionKind.BitwiseNotOperatorMethodType, +]); + +const requiresLua53 = new Set([ + ...bitwiseOperatorMapExtensions, + ExtensionKind.FloorDivisionOperatorType, + ExtensionKind.FloorDivisionOperatorMethodType, +]); + +export const operatorExtensionTransformers: LanguageExtensionCallTransformerMap = {}; +for (const kind of binaryOperatorMappings.keys()) { + operatorExtensionTransformers[kind] = transformBinaryOperator; +} +for (const kind of unaryOperatorMappings.keys()) { + operatorExtensionTransformers[kind] = transformUnaryOperator; +} + +function transformBinaryOperator(context: TransformationContext, node: ts.CallExpression, kind: ExtensionKind) { + if (requiresLua53.has(kind)) checkHasLua53(context, node, kind); + + const args = getBinaryCallExtensionArgs(context, node, kind); + if (!args) return lua.createNilLiteral(); + + const [left, right] = transformOrderedExpressions(context, args); + + const luaOperator = binaryOperatorMappings.get(kind); + assert(luaOperator); + return lua.createBinaryExpression(left, right, luaOperator); +} + +function transformUnaryOperator(context: TransformationContext, node: ts.CallExpression, kind: ExtensionKind) { + if (requiresLua53.has(kind)) checkHasLua53(context, node, kind); + + const arg = getUnaryCallExtensionArg(context, node, kind); + if (!arg) return lua.createNilLiteral(); + + const luaOperator = unaryOperatorMappings.get(kind); + assert(luaOperator); + return lua.createUnaryExpression(context.transformExpression(arg), luaOperator); +} + +function checkHasLua53(context: TransformationContext, node: ts.CallExpression, kind: ExtensionKind) { + const isBefore53 = + context.luaTarget === LuaTarget.Lua50 || + context.luaTarget === LuaTarget.Lua51 || + context.luaTarget === LuaTarget.Lua52 || + context.luaTarget === LuaTarget.LuaJIT || + context.luaTarget === LuaTarget.Universal; + if (isBefore53) { + const luaTarget = context.luaTarget === LuaTarget.Universal ? LuaTarget.Lua51 : context.luaTarget; + if ( + kind === ExtensionKind.FloorDivisionOperatorType || + kind === ExtensionKind.FloorDivisionOperatorMethodType + ) { + context.diagnostics.push(unsupportedForTarget(node, "Floor division operator", luaTarget)); + } else { + // is bitwise operator + context.diagnostics.push(unsupportedForTarget(node, "Native bitwise operations", luaTarget)); + } + } +} diff --git a/src/transformation/visitors/language-extensions/range.ts b/src/transformation/visitors/language-extensions/range.ts new file mode 100644 index 000000000..00647180c --- /dev/null +++ b/src/transformation/visitors/language-extensions/range.ts @@ -0,0 +1,53 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import * as extensions from "../../utils/language-extensions"; +import { TransformationContext } from "../../context"; +import { getVariableDeclarationBinding } from "../loops/utils"; +import { transformIdentifier } from "../identifier"; +import { transformArguments } from "../call"; +import { assert } from "../../../utils"; +import { invalidRangeControlVariable } from "../../utils/diagnostics"; +import { getExtensionKindForNode } from "../../utils/language-extensions"; + +export function isRangeFunction(context: TransformationContext, expression: ts.CallExpression): boolean { + return isRangeFunctionNode(context, expression.expression); +} + +export function isRangeFunctionNode(context: TransformationContext, node: ts.Node): boolean { + return ( + ts.isIdentifier(node) && + node.text === "$range" && + getExtensionKindForNode(context, node) === extensions.ExtensionKind.RangeFunction + ); +} + +function getControlVariable(context: TransformationContext, statement: ts.ForOfStatement) { + if (!ts.isVariableDeclarationList(statement.initializer)) { + context.diagnostics.push(invalidRangeControlVariable(statement.initializer)); + return; + } + + const binding = getVariableDeclarationBinding(context, statement.initializer); + if (!ts.isIdentifier(binding)) { + context.diagnostics.push(invalidRangeControlVariable(statement.initializer)); + return; + } + + return transformIdentifier(context, binding); +} + +export function transformRangeStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block +): lua.Statement { + assert(ts.isCallExpression(statement.expression)); + const controlVariable = + getControlVariable(context, statement) ?? lua.createAnonymousIdentifier(statement.initializer); + const [start = lua.createNumericLiteral(0), limit = lua.createNumericLiteral(0), step] = transformArguments( + context, + statement.expression.arguments, + context.checker.getResolvedSignature(statement.expression) + ); + return lua.createForStatement(block, controlVariable, start, limit, step, statement); +} diff --git a/src/transformation/visitors/language-extensions/table.ts b/src/transformation/visitors/language-extensions/table.ts new file mode 100644 index 000000000..e42e3f8c7 --- /dev/null +++ b/src/transformation/visitors/language-extensions/table.ts @@ -0,0 +1,145 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { TransformationContext } from "../../context"; +import { + ExtensionKind, + getBinaryCallExtensionArgs, + getExtensionKindForNode, + getNaryCallExtensionArgs, + getUnaryCallExtensionArg, +} from "../../utils/language-extensions"; +import { transformOrderedExpressions } from "../expression-list"; +import { LanguageExtensionCallTransformerMap } from "./call-extension"; + +export function isTableNewCall(context: TransformationContext, node: ts.NewExpression) { + return getExtensionKindForNode(context, node.expression) === ExtensionKind.TableNewType; +} + +export const tableNewExtensions = [ExtensionKind.TableNewType]; + +export const tableExtensionTransformers: LanguageExtensionCallTransformerMap = { + [ExtensionKind.TableDeleteType]: transformTableDeleteExpression, + [ExtensionKind.TableDeleteMethodType]: transformTableDeleteExpression, + [ExtensionKind.TableGetType]: transformTableGetExpression, + [ExtensionKind.TableGetMethodType]: transformTableGetExpression, + [ExtensionKind.TableHasType]: transformTableHasExpression, + [ExtensionKind.TableHasMethodType]: transformTableHasExpression, + [ExtensionKind.TableSetType]: transformTableSetExpression, + [ExtensionKind.TableSetMethodType]: transformTableSetExpression, + [ExtensionKind.TableAddKeyType]: transformTableAddKeyExpression, + [ExtensionKind.TableAddKeyMethodType]: transformTableAddKeyExpression, + [ExtensionKind.TableIsEmptyType]: transformTableIsEmptyExpression, + [ExtensionKind.TableIsEmptyMethodType]: transformTableIsEmptyExpression, +}; + +function transformTableDeleteExpression( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +): lua.Expression { + const args = getBinaryCallExtensionArgs(context, node, extensionKind); + if (!args) { + return lua.createNilLiteral(); + } + + const [table, key] = transformOrderedExpressions(context, args); + // arg0[arg1] = nil + context.addPrecedingStatements( + lua.createAssignmentStatement(lua.createTableIndexExpression(table, key), lua.createNilLiteral(), node) + ); + return lua.createBooleanLiteral(true); +} + +function transformTableGetExpression( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +): lua.Expression { + const args = getBinaryCallExtensionArgs(context, node, extensionKind); + if (!args) { + return lua.createNilLiteral(); + } + + const [table, key] = transformOrderedExpressions(context, args); + // arg0[arg1] + return lua.createTableIndexExpression(table, key, node); +} + +function transformTableHasExpression( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +): lua.Expression { + const args = getBinaryCallExtensionArgs(context, node, extensionKind); + if (!args) { + return lua.createNilLiteral(); + } + + const [table, key] = transformOrderedExpressions(context, args); + // arg0[arg1] + const tableIndexExpression = lua.createTableIndexExpression(table, key); + + // arg0[arg1] ~= nil + return lua.createBinaryExpression( + tableIndexExpression, + lua.createNilLiteral(), + lua.SyntaxKind.InequalityOperator, + node + ); +} + +function transformTableSetExpression( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +): lua.Expression { + const args = getNaryCallExtensionArgs(context, node, extensionKind, 3); + if (!args) { + return lua.createNilLiteral(); + } + + const [table, key, value] = transformOrderedExpressions(context, args); + // arg0[arg1] = arg2 + context.addPrecedingStatements( + lua.createAssignmentStatement(lua.createTableIndexExpression(table, key), value, node) + ); + return lua.createNilLiteral(); +} + +function transformTableAddKeyExpression( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +): lua.Expression { + const args = getNaryCallExtensionArgs(context, node, extensionKind, 2); + if (!args) { + return lua.createNilLiteral(); + } + + const [table, key] = transformOrderedExpressions(context, args); + // arg0[arg1] = true + context.addPrecedingStatements( + lua.createAssignmentStatement(lua.createTableIndexExpression(table, key), lua.createBooleanLiteral(true), node) + ); + return lua.createNilLiteral(); +} + +function transformTableIsEmptyExpression( + context: TransformationContext, + node: ts.CallExpression, + extensionKind: ExtensionKind +): lua.Expression { + const args = getUnaryCallExtensionArg(context, node, extensionKind); + if (!args) { + return lua.createNilLiteral(); + } + + const table = context.transformExpression(args); + // next(arg0) == nil + return lua.createBinaryExpression( + lua.createCallExpression(lua.createIdentifier("next"), [table], node), + lua.createNilLiteral(), + lua.SyntaxKind.EqualityOperator, + node + ); +} diff --git a/src/transformation/visitors/language-extensions/vararg.ts b/src/transformation/visitors/language-extensions/vararg.ts new file mode 100644 index 000000000..4576737c8 --- /dev/null +++ b/src/transformation/visitors/language-extensions/vararg.ts @@ -0,0 +1,20 @@ +import * as ts from "typescript"; +import { TransformationContext } from "../../context"; +import * as extensions from "../../utils/language-extensions"; +import { getExtensionKindForSymbol } from "../../utils/language-extensions"; +import { Scope, ScopeType } from "../../utils/scope"; + +export function isGlobalVarargConstant(context: TransformationContext, symbol: ts.Symbol, scope: Scope) { + return scope.type === ScopeType.File && isVarargConstantSymbol(context, symbol); +} +function isVarargConstantSymbol(context: TransformationContext, symbol: ts.Symbol) { + return ( + symbol.getName() === "$vararg" && + getExtensionKindForSymbol(context, symbol) === extensions.ExtensionKind.VarargConstant + ); +} + +export function isVarargConstantNode(context: TransformationContext, node: ts.Node): boolean { + const symbol = context.checker.getSymbolAtLocation(node); + return symbol !== undefined && isVarargConstantSymbol(context, symbol); +} diff --git a/src/transformation/visitors/literal.ts b/src/transformation/visitors/literal.ts new file mode 100644 index 000000000..1d91c89d9 --- /dev/null +++ b/src/transformation/visitors/literal.ts @@ -0,0 +1,224 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { assertNever } from "../../utils"; +import { FunctionVisitor, TransformationContext, Visitors } from "../context"; +import { undefinedInArrayLiteral, unsupportedAccessorInObjectLiteral } from "../utils/diagnostics"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { trackSymbolReference } from "../utils/symbols"; +import { isArrayType } from "../utils/typescript"; +import { transformFunctionLikeDeclaration } from "./function"; +import { moveToPrecedingTemp, transformExpressionList } from "./expression-list"; +import { transformIdentifierWithSymbol } from "./identifier"; +import { LuaTarget } from "../../CompilerOptions"; + +// TODO: Move to object-literal.ts? +export function transformPropertyName(context: TransformationContext, node: ts.PropertyName): lua.Expression { + if (ts.isComputedPropertyName(node)) { + return context.transformExpression(node.expression); + } else if (ts.isIdentifier(node)) { + return lua.createStringLiteral(node.text); + } else if (ts.isPrivateIdentifier(node)) { + throw new Error("PrivateIdentifier is not supported"); + } else { + return context.transformExpression(node); + } +} + +export function createShorthandIdentifier( + context: TransformationContext, + valueSymbol: ts.Symbol | undefined, + propertyIdentifier: ts.Identifier +): lua.Expression { + return transformIdentifierWithSymbol(context, propertyIdentifier, valueSymbol); +} + +const transformNumericLiteralExpression: FunctionVisitor = (expression, context) => { + if (expression.text === "Infinity") { + if (context.luaTarget === LuaTarget.Lua50) { + const one = lua.createNumericLiteral(1); + const zero = lua.createNumericLiteral(0); + return lua.createBinaryExpression(one, zero, lua.SyntaxKind.DivisionOperator); + } else { + const math = lua.createIdentifier("math"); + const huge = lua.createStringLiteral("huge"); + return lua.createTableIndexExpression(math, huge, expression); + } + } + + return lua.createNumericLiteral(Number(expression.text), expression); +}; + +const transformObjectLiteralExpression: FunctionVisitor = (expression, context) => { + const properties: lua.Expression[] = []; + const initializers: ts.Node[] = []; + const keyPrecedingStatements: lua.Statement[][] = []; + const valuePrecedingStatements: lua.Statement[][] = []; + let lastPrecedingStatementsIndex = -1; + + for (let i = 0; i < expression.properties.length; ++i) { + const element = expression.properties[i]; + + // Transform key and cache preceding statements + context.pushPrecedingStatements(); + + const name = element.name ? transformPropertyName(context, element.name) : undefined; + + let precedingStatements = context.popPrecedingStatements(); + keyPrecedingStatements.push(precedingStatements); + if (precedingStatements.length > 0) { + lastPrecedingStatementsIndex = i; + } + + // Transform value and cache preceding statements + context.pushPrecedingStatements(); + + if (ts.isPropertyAssignment(element)) { + const expression = context.transformExpression(element.initializer); + properties.push(lua.createTableFieldExpression(expression, name, element)); + initializers.push(element.initializer); + } else if (ts.isShorthandPropertyAssignment(element)) { + const valueSymbol = context.checker.getShorthandAssignmentValueSymbol(element); + if (valueSymbol) { + trackSymbolReference(context, valueSymbol, element.name); + } + + const identifier = createShorthandIdentifier(context, valueSymbol, element.name); + properties.push(lua.createTableFieldExpression(identifier, name, element)); + initializers.push(element); + } else if (ts.isMethodDeclaration(element)) { + const expression = transformFunctionLikeDeclaration(element, context); + properties.push(lua.createTableFieldExpression(expression, name, element)); + initializers.push(element); + } else if (ts.isSpreadAssignment(element)) { + const type = context.checker.getTypeAtLocation(element.expression); + let tableExpression: lua.Expression; + if (isArrayType(context, type)) { + tableExpression = transformLuaLibFunction( + context, + LuaLibFeature.ArrayToObject, + element.expression, + context.transformExpression(element.expression) + ); + } else { + tableExpression = context.transformExpression(element.expression); + } + + properties.push(tableExpression); + initializers.push(element.expression); + } else if (ts.isAccessor(element)) { + context.diagnostics.push(unsupportedAccessorInObjectLiteral(element)); + } else { + assertNever(element); + } + + precedingStatements = context.popPrecedingStatements(); + valuePrecedingStatements.push(precedingStatements); + if (precedingStatements.length > 0) { + lastPrecedingStatementsIndex = i; + } + } + + // Expressions referenced before others that produced preceding statements need to be cached in temps + if (lastPrecedingStatementsIndex >= 0) { + for (let i = 0; i < properties.length; ++i) { + const property = properties[i]; + + // Bubble up key's preceding statements + context.addPrecedingStatements(keyPrecedingStatements[i]); + + // Cache computed property name in temp if before the last expression that generated preceding statements + if (i <= lastPrecedingStatementsIndex && lua.isTableFieldExpression(property) && property.key) { + property.key = moveToPrecedingTemp(context, property.key, expression.properties[i].name); + } + + // Bubble up value's preceding statements + context.addPrecedingStatements(valuePrecedingStatements[i]); + + // Cache property value in temp if before the last expression that generated preceding statements + if (i < lastPrecedingStatementsIndex) { + if (lua.isTableFieldExpression(property)) { + property.value = moveToPrecedingTemp(context, property.value, initializers[i]); + } else { + properties[i] = moveToPrecedingTemp(context, property, initializers[i]); + } + } + } + } + + // Sort into field expressions and tables to pass into __TS__ObjectAssign + let fields: lua.TableFieldExpression[] = []; + const tableExpressions: lua.Expression[] = []; + for (const property of properties) { + if (lua.isTableFieldExpression(property)) { + fields.push(property); + } else { + if (fields.length > 0) { + tableExpressions.push(lua.createTableExpression(fields)); + } + tableExpressions.push(property); + fields = []; + } + } + + if (tableExpressions.length === 0) { + return lua.createTableExpression(fields, expression); + } else { + if (fields.length > 0) { + const tableExpression = lua.createTableExpression(fields, expression); + tableExpressions.push(tableExpression); + } + + if (tableExpressions[0].kind !== lua.SyntaxKind.TableExpression) { + tableExpressions.unshift(lua.createTableExpression(undefined, expression)); + } + return transformLuaLibFunction(context, LuaLibFeature.ObjectAssign, expression, ...tableExpressions); + } +}; + +const transformArrayLiteralExpression: FunctionVisitor = (expression, context) => { + // Disallow using undefined/null in array literals + checkForUndefinedOrNullInArrayLiteral(expression, context); + + const filteredElements = expression.elements.map(e => + ts.isOmittedExpression(e) ? ts.factory.createIdentifier("undefined") : e + ); + const values = transformExpressionList(context, filteredElements).map(e => lua.createTableFieldExpression(e)); + + return lua.createTableExpression(values, expression); +}; + +function checkForUndefinedOrNullInArrayLiteral(array: ts.ArrayLiteralExpression, context: TransformationContext) { + // Look for last non-nil element in literal + let lastNonUndefinedIndex = array.elements.length - 1; + for (; lastNonUndefinedIndex >= 0; lastNonUndefinedIndex--) { + if (!isUndefinedOrNull(array.elements[lastNonUndefinedIndex])) { + break; + } + } + + // Add diagnostics for non-trailing nil elements in array literal + for (let i = 0; i < array.elements.length; i++) { + if (i < lastNonUndefinedIndex && isUndefinedOrNull(array.elements[i])) { + context.diagnostics.push(undefinedInArrayLiteral(array.elements[i])); + } + } +} + +function isUndefinedOrNull(node: ts.Node) { + return ( + node.kind === ts.SyntaxKind.UndefinedKeyword || + node.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(node) && node.text === "undefined") + ); +} + +export const literalVisitors: Visitors = { + [ts.SyntaxKind.NullKeyword]: node => lua.createNilLiteral(node), + [ts.SyntaxKind.TrueKeyword]: node => lua.createBooleanLiteral(true, node), + [ts.SyntaxKind.FalseKeyword]: node => lua.createBooleanLiteral(false, node), + [ts.SyntaxKind.NumericLiteral]: transformNumericLiteralExpression, + [ts.SyntaxKind.StringLiteral]: node => lua.createStringLiteral(node.text, node), + [ts.SyntaxKind.NoSubstitutionTemplateLiteral]: node => lua.createStringLiteral(node.text, node), + [ts.SyntaxKind.ObjectLiteralExpression]: transformObjectLiteralExpression, + [ts.SyntaxKind.ArrayLiteralExpression]: transformArrayLiteralExpression, +}; diff --git a/src/transformation/visitors/loops/do-while.ts b/src/transformation/visitors/loops/do-while.ts new file mode 100644 index 000000000..f4c714c71 --- /dev/null +++ b/src/transformation/visitors/loops/do-while.ts @@ -0,0 +1,77 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { FunctionVisitor } from "../../context"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; +import { checkOnlyTruthyCondition } from "../conditional"; +import { invertCondition, transformLoopBody } from "./utils"; + +export const transformWhileStatement: FunctionVisitor = (statement, context) => { + // Check if we need to add diagnostic about Lua truthiness + checkOnlyTruthyCondition(statement.expression, context); + + const body = transformLoopBody(context, statement); + + let { precedingStatements: conditionPrecedingStatements, result: condition } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(statement.expression) + ); + + // If condition has preceding statements, ensure they are executed every iteration by using the form: + // + // while true do + // condition's preceding statements + // if not condition then + // break + // end + // ... + // end + if (conditionPrecedingStatements.length > 0) { + conditionPrecedingStatements.push( + lua.createIfStatement( + invertCondition(condition), + lua.createBlock([lua.createBreakStatement()]), + undefined, + statement.expression + ) + ); + body.unshift(...conditionPrecedingStatements); + condition = lua.createBooleanLiteral(true); + } + + return lua.createWhileStatement(lua.createBlock(body), condition, statement); +}; + +export const transformDoStatement: FunctionVisitor = (statement, context) => { + // Check if we need to add diagnostic about Lua truthiness + checkOnlyTruthyCondition(statement.expression, context); + + const body = lua.createDoStatement(transformLoopBody(context, statement)); + + let { precedingStatements: conditionPrecedingStatements, result: condition } = transformInPrecedingStatementScope( + context, + () => invertCondition(context.transformExpression(statement.expression)) + ); + + // If condition has preceding statements, ensure they are executed every iteration by using the form: + // + // repeat + // ... + // condition's preceding statements + // if condition then + // break + // end + // end + if (conditionPrecedingStatements.length > 0) { + conditionPrecedingStatements.push( + lua.createIfStatement( + condition, + lua.createBlock([lua.createBreakStatement()]), + undefined, + statement.expression + ) + ); + condition = lua.createBooleanLiteral(false); + } + + return lua.createRepeatStatement(lua.createBlock([body, ...conditionPrecedingStatements]), condition, statement); +}; diff --git a/src/transformation/visitors/loops/for-in.ts b/src/transformation/visitors/loops/for-in.ts new file mode 100644 index 000000000..296045056 --- /dev/null +++ b/src/transformation/visitors/loops/for-in.ts @@ -0,0 +1,22 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { FunctionVisitor } from "../../context"; +import { forbiddenForIn } from "../../utils/diagnostics"; +import { isArrayType } from "../../utils/typescript"; +import { transformForInitializer, transformLoopBody } from "./utils"; + +export const transformForInStatement: FunctionVisitor = (statement, context) => { + if (isArrayType(context, context.checker.getTypeAtLocation(statement.expression))) { + context.diagnostics.push(forbiddenForIn(statement)); + } + + // Transpile expression + const pairsIdentifier = lua.createIdentifier("pairs"); + const expression = context.transformExpression(statement.expression); + const pairsCall = lua.createCallExpression(pairsIdentifier, [expression]); + + const body = lua.createBlock(transformLoopBody(context, statement)); + + const valueVariable = transformForInitializer(context, statement.initializer, body); + return lua.createForInStatement(body, [valueVariable], [pairsCall], statement); +}; diff --git a/src/transformation/visitors/loops/for-of.ts b/src/transformation/visitors/loops/for-of.ts new file mode 100644 index 000000000..c2da717c5 --- /dev/null +++ b/src/transformation/visitors/loops/for-of.ts @@ -0,0 +1,68 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../../context"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { isArrayType } from "../../utils/typescript"; +import { + transformForOfIterableStatement, + transformForOfPairsIterableStatement, + transformForOfPairsKeyIterableStatement, +} from "../language-extensions/iterable"; +import { isRangeFunction, transformRangeStatement } from "../language-extensions/range"; +import { transformForInitializer, transformLoopBody } from "./utils"; +import { getIterableExtensionKindForNode, IterableExtensionKind } from "../../utils/language-extensions"; +import { assertNever } from "../../../utils"; + +function transformForOfArrayStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block +): lua.Statement { + const valueVariable = transformForInitializer(context, statement.initializer, block); + const ipairsCall = lua.createCallExpression(lua.createIdentifier("ipairs"), [ + context.transformExpression(statement.expression), + ]); + + return lua.createForInStatement(block, [lua.createAnonymousIdentifier(), valueVariable], [ipairsCall], statement); +} + +function transformForOfIteratorStatement( + context: TransformationContext, + statement: ts.ForOfStatement, + block: lua.Block +): lua.Statement { + const valueVariable = transformForInitializer(context, statement.initializer, block); + const iterable = transformLuaLibFunction( + context, + LuaLibFeature.Iterator, + statement.expression, + context.transformExpression(statement.expression) + ); + + return lua.createForInStatement(block, [lua.createAnonymousIdentifier(), valueVariable], [iterable], statement); +} + +export const transformForOfStatement: FunctionVisitor = (node, context) => { + const body = lua.createBlock(transformLoopBody(context, node)); + + if (ts.isCallExpression(node.expression) && isRangeFunction(context, node.expression)) { + return transformRangeStatement(context, node, body); + } + const iterableExtensionType = getIterableExtensionKindForNode(context, node.expression); + if (iterableExtensionType) { + if (iterableExtensionType === IterableExtensionKind.Iterable) { + return transformForOfIterableStatement(context, node, body); + } else if (iterableExtensionType === IterableExtensionKind.Pairs) { + return transformForOfPairsIterableStatement(context, node, body); + } else if (iterableExtensionType === IterableExtensionKind.PairsKey) { + return transformForOfPairsKeyIterableStatement(context, node, body); + } else { + assertNever(iterableExtensionType); + } + } + if (isArrayType(context, context.checker.getTypeAtLocation(node.expression))) { + return transformForOfArrayStatement(context, node, body); + } + + return transformForOfIteratorStatement(context, node, body); +}; diff --git a/src/transformation/visitors/loops/for.ts b/src/transformation/visitors/loops/for.ts new file mode 100644 index 000000000..5ae3bf0ea --- /dev/null +++ b/src/transformation/visitors/loops/for.ts @@ -0,0 +1,70 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +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); + // local initializer = value + result.push(...statement.initializer.declarations.flatMap(d => transformVariableDeclaration(context, d))); + } else { + result.push(...context.transformStatements(ts.factory.createExpressionStatement(statement.initializer))); + } + } + + const body: lua.Statement[] = transformLoopBody(context, statement); + + let condition: lua.Expression; + if (statement.condition) { + const tsCondition = statement.condition; + const { precedingStatements: conditionPrecedingStatements, result } = transformInPrecedingStatementScope( + context, + () => context.transformExpression(tsCondition) + ); + condition = result; + + // If condition has preceding statements, ensure they are executed every iteration by using the form: + // + // while true do + // condition's preceding statements + // if not condition then + // break + // end + // ... + // end + if (conditionPrecedingStatements.length > 0) { + conditionPrecedingStatements.push( + lua.createIfStatement( + invertCondition(condition), + lua.createBlock([lua.createBreakStatement()]), + undefined, + statement.condition + ) + ); + body.unshift(...conditionPrecedingStatements); + condition = lua.createBooleanLiteral(true); + } + } else { + condition = lua.createBooleanLiteral(true); + } + + if (statement.incrementor) { + body.push(...context.transformStatements(ts.factory.createExpressionStatement(statement.incrementor))); + } + + // 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 new file mode 100644 index 000000000..d7e4a3093 --- /dev/null +++ b/src/transformation/visitors/loops/utils.ts @@ -0,0 +1,120 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { TransformationContext } from "../../context"; +import { transformInPrecedingStatementScope } from "../../utils/preceding-statements"; +import { LoopContinued, performHoisting, ScopeType } from "../../utils/scope"; +import { isAssignmentPattern } from "../../utils/typescript"; +import { transformAssignment } from "../binary-expression/assignments"; +import { transformAssignmentPattern } from "../binary-expression/destructuring-assignments"; +import { transformBlockOrStatement } from "../block"; +import { transformIdentifier } from "../identifier"; +import { checkVariableDeclarationList, transformBindingPattern } from "../variable-declaration"; + +export function transformLoopBody( + context: TransformationContext, + loop: ts.WhileStatement | ts.DoStatement | ts.ForStatement | ts.ForOfStatement | ts.ForInOrOfStatement +): lua.Statement[] { + 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: + return [lua.createDoStatement(body), lua.createLabelStatement(`__continue${scopeId}`)]; + + case LoopContinued.WithRepeatBreak: + 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(transformedBodyStatements), literalTrue), + lua.createIfStatement( + lua.createUnaryExpression(identifier, lua.SyntaxKind.NotOperator), + lua.createBlock([lua.createBreakStatement()]) + ), + ]), + ]; + } +} + +export function getVariableDeclarationBinding( + context: TransformationContext, + node: ts.VariableDeclarationList +): ts.BindingName { + checkVariableDeclarationList(context, node); + + if (node.declarations.length === 0) { + return ts.factory.createIdentifier("____"); + } + + return node.declarations[0].name; +} + +export function transformForInitializer( + context: TransformationContext, + initializer: ts.ForInitializer, + block: lua.Block +): lua.Identifier { + const valueVariable = lua.createIdentifier("____value"); + + context.pushScope(ScopeType.LoopInitializer, initializer); + + if (ts.isVariableDeclarationList(initializer)) { + // Declaration of new variable + + const binding = getVariableDeclarationBinding(context, initializer); + if (ts.isArrayBindingPattern(binding) || ts.isObjectBindingPattern(binding)) { + const { precedingStatements, result: bindings } = transformInPrecedingStatementScope(context, () => + transformBindingPattern(context, binding, valueVariable) + ); + block.statements.unshift(...precedingStatements, ...bindings); + } else { + // Single variable declared in for loop + context.popScope(); + return transformIdentifier(context, binding); + } + } else { + // Assignment to existing variable(s) + + block.statements.unshift( + ...(isAssignmentPattern(initializer) + ? transformAssignmentPattern(context, initializer, valueVariable, false) + : transformAssignment(context, initializer, valueVariable)) + ); + } + + context.popScope(); + return valueVariable; +} + +export function invertCondition(expression: lua.Expression) { + if (lua.isUnaryExpression(expression) && expression.operator === lua.SyntaxKind.NotOperator) { + return expression.operand; + } else { + const notExpression = lua.createUnaryExpression(expression, lua.SyntaxKind.NotOperator); + lua.setNodePosition(notExpression, lua.getOriginalPos(expression)); + return notExpression; + } +} diff --git a/src/transformation/visitors/modules/export.ts b/src/transformation/visitors/modules/export.ts new file mode 100644 index 000000000..c814b8257 --- /dev/null +++ b/src/transformation/visitors/modules/export.ts @@ -0,0 +1,194 @@ +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { assert } from "../../../utils"; +import { FunctionVisitor, TransformationContext } from "../../context"; +import { createDefaultExportExpression, createDefaultExportStringLiteral } from "../../utils/export"; +import { createExportsIdentifier } from "../../utils/lua-ast"; +import { createShorthandIdentifier, transformPropertyName } from "../literal"; +import { createModuleRequire } from "./import"; +import { createSafeName } from "../../utils/safe-names"; +import * as path from "path"; + +export const transformExportAssignment: FunctionVisitor = (node, context) => { + if (!context.resolver.isValueAliasDeclaration(node)) { + return undefined; + } + + const exportedValue = context.transformExpression(node.expression); + + // export = [expression]; + // ____exports = [expression]; + if (node.isExportEquals) { + return lua.createVariableDeclarationStatement(createExportsIdentifier(), exportedValue, node); + } else { + // export default [expression]; + // ____exports.default = [expression]; + return lua.createAssignmentStatement( + lua.createTableIndexExpression(createExportsIdentifier(), createDefaultExportStringLiteral(node)), + exportedValue, + node + ); + } +}; + +function transformExportAll(context: TransformationContext, node: ts.ExportDeclaration): lua.Statement | undefined { + assert(node.moduleSpecifier); + + const moduleRequire = createModuleRequire(context, node.moduleSpecifier); + + // export * as ns from "..."; + // exports.ns = require(...) + if (node.exportClause && ts.isNamespaceExport(node.exportClause)) { + const assignToExports = lua.createAssignmentStatement( + lua.createTableIndexExpression( + createExportsIdentifier(), + lua.createStringLiteral(node.exportClause.name.text) + ), + moduleRequire + ); + return assignToExports; + } + + // export * from "..."; + // exports all values EXCEPT "default" from "..." + const result: lua.Statement[] = []; + + // local ____export = require(...) + const tempModuleIdentifier = lua.createIdentifier("____export"); + const declaration = lua.createVariableDeclarationStatement(tempModuleIdentifier, moduleRequire); + result.push(declaration); + + // ____exports[____exportKey] = ____exportValue + const forKey = lua.createIdentifier("____exportKey"); + const forValue = lua.createIdentifier("____exportValue"); + const leftAssignment = lua.createAssignmentStatement( + lua.createTableIndexExpression(createExportsIdentifier(), forKey), + forValue + ); + + // if key ~= "default" then + // -- export the value, do not export "default" values + // end + const ifBody = lua.createBlock([leftAssignment]); + const ifStatement = lua.createIfStatement( + lua.createBinaryExpression( + lua.cloneIdentifier(forKey), + lua.createStringLiteral("default"), + lua.SyntaxKind.InequalityOperator + ), + ifBody + ); + + // for ____exportKey, ____exportValue in ____export do + // -- export ____exportValue, unless ____exportKey is "default" + // end + const pairsIdentifier = lua.createIdentifier("pairs"); + const forIn = lua.createForInStatement( + lua.createBlock([ifStatement]), + [lua.cloneIdentifier(forKey), lua.cloneIdentifier(forValue)], + [lua.createCallExpression(pairsIdentifier, [lua.cloneIdentifier(tempModuleIdentifier)])] + ); + + result.push(forIn); + + // Wrap this in a DoStatement to prevent polluting the scope. + return lua.createDoStatement(result, node); +} + +const isDefaultExportSpecifier = (node: ts.ExportSpecifier) => + (node.name && + ts.isIdentifier(node.name) && + ts.identifierToKeywordKind(node.name) === ts.SyntaxKind.DefaultKeyword) || + (node.propertyName && + ts.isIdentifier(node.propertyName) && + ts.identifierToKeywordKind(node.propertyName) === ts.SyntaxKind.DefaultKeyword); + +function transformExportSpecifier(context: TransformationContext, node: ts.ExportSpecifier): lua.AssignmentStatement { + const exportedName = node.name; + const exportedValue = node.propertyName ?? node.name; + let rhs: lua.Expression; + if (ts.isIdentifier(exportedValue)) { + const exportedSymbol = context.checker.getExportSpecifierLocalTargetSymbol(node); + rhs = createShorthandIdentifier(context, exportedSymbol, exportedValue); + } else { + rhs = lua.createStringLiteral(exportedName.text, exportedValue); + } + + if (isDefaultExportSpecifier(node)) { + const lhs = createDefaultExportExpression(node); + return lua.createAssignmentStatement(lhs, rhs, node); + } else { + const exportsTable = createExportsIdentifier(); + const lhs = lua.createTableIndexExpression( + exportsTable, + lua.createStringLiteral(exportedName.text), + exportedName + ); + + return lua.createAssignmentStatement(lhs, rhs, node); + } +} + +function transformExportSpecifiersFrom( + context: TransformationContext, + statement: ts.ExportDeclaration, + moduleSpecifier: ts.Expression, + exportSpecifiers: ts.ExportSpecifier[] +): lua.Statement { + const result: lua.Statement[] = []; + + const importPath = ts.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text.replace(/"/g, "") : "module"; + + // Create the require statement to extract values. + // local ____module = require("module") + const importUniqueName = lua.createIdentifier(createSafeName(path.basename(importPath))); + const requireCall = createModuleRequire(context, moduleSpecifier); + result.push(lua.createVariableDeclarationStatement(importUniqueName, requireCall, statement)); + + for (const specifier of exportSpecifiers) { + // Assign to exports table + const exportsTable = createExportsIdentifier(); + const exportedName = specifier.name; + const exportedNameTransformed = transformPropertyName(context, exportedName); + const lhs = lua.createTableIndexExpression(exportsTable, exportedNameTransformed, exportedName); + + const exportedValue = specifier.propertyName ?? specifier.name; + const rhs = lua.createTableIndexExpression( + lua.cloneIdentifier(importUniqueName), + transformPropertyName(context, exportedValue), + specifier + ); + result.push(lua.createAssignmentStatement(lhs, rhs, specifier)); + } + + return lua.createDoStatement(result, statement); +} + +export const getExported = (context: TransformationContext, exportSpecifiers: ts.NamedExports) => + exportSpecifiers.elements.filter(exportSpecifier => context.resolver.isValueAliasDeclaration(exportSpecifier)); + +export const transformExportDeclaration: FunctionVisitor = (node, context) => { + if (!node.exportClause) { + // export * from "..."; + return transformExportAll(context, node); + } + + if (!context.resolver.isValueAliasDeclaration(node)) { + return undefined; + } + + if (ts.isNamespaceExport(node.exportClause)) { + // export * as ns from "..."; + return transformExportAll(context, node); + } + + const exportSpecifiers = getExported(context, node.exportClause); + + // export { ... }; + if (!node.moduleSpecifier) { + return exportSpecifiers.map(exportSpecifier => transformExportSpecifier(context, exportSpecifier)); + } + + // export { ... } from "..."; + return transformExportSpecifiersFrom(context, node, node.moduleSpecifier, exportSpecifiers); +}; diff --git a/src/transformation/visitors/modules/import.ts b/src/transformation/visitors/modules/import.ts new file mode 100644 index 000000000..7c6a597ac --- /dev/null +++ b/src/transformation/visitors/modules/import.ts @@ -0,0 +1,175 @@ +import * as path from "path"; +import * as ts from "typescript"; +import * as lua from "../../../LuaAST"; +import { createStaticPromiseFunctionAccessor } from "../../builtins/promise"; +import { FunctionVisitor, TransformationContext } from "../../context"; +import { AnnotationKind, getSymbolAnnotations } from "../../utils/annotations"; +import { createDefaultExportStringLiteral } from "../../utils/export"; +import { createHoistableVariableDeclarationStatement } from "../../utils/lua-ast"; +import { importLuaLibFeature, LuaLibFeature } from "../../utils/lualib"; +import { createSafeName } from "../../utils/safe-names"; +import { peekScope } from "../../utils/scope"; +import { getCustomNameFromSymbol, transformIdentifier } from "../identifier"; +import { transformPropertyName } from "../literal"; + +function isNoResolutionPath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean { + const moduleOwnerSymbol = context.checker.getSymbolAtLocation(moduleSpecifier); + if (!moduleOwnerSymbol) return false; + + const annotations = getSymbolAnnotations(moduleOwnerSymbol); + return annotations.has(AnnotationKind.NoResolution); +} + +export function createModuleRequire( + context: TransformationContext, + moduleSpecifier: ts.Expression, + tsOriginal: ts.Node = moduleSpecifier +): lua.CallExpression { + const params: lua.Expression[] = []; + if (ts.isStringLiteral(moduleSpecifier)) { + const modulePath = isNoResolutionPath(context, moduleSpecifier) + ? `@NoResolution:${moduleSpecifier.text}` + : moduleSpecifier.text; + + params.push(lua.createStringLiteral(modulePath)); + } + + return lua.createCallExpression(lua.createIdentifier("require"), params, tsOriginal); +} + +function shouldBeImported(context: TransformationContext, importNode: ts.ImportClause | ts.ImportSpecifier): boolean { + return context.resolver.isReferencedAliasDeclaration(importNode); +} + +function transformImportSpecifier( + context: TransformationContext, + importSpecifier: ts.ImportSpecifier, + moduleTableName: lua.Identifier +): lua.VariableDeclarationStatement { + const type = context.checker.getTypeAtLocation(importSpecifier.name); + + const leftIdentifier = transformIdentifier(context, 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, + lua.createTableIndexExpression(moduleTableName, propertyName), + importSpecifier + ); +} + +export const transformImportDeclaration: FunctionVisitor = (statement, context) => { + const scope = peekScope(context); + + scope.importStatements ??= []; + + const result: lua.Statement[] = []; + const requireCall = createModuleRequire(context, statement.moduleSpecifier); + + // import "./module"; + // require("module") + if (statement.importClause === undefined) { + result.push(lua.createExpressionStatement(requireCall)); + + scope.importStatements.push(...result); + return undefined; + } + + const importPath = ts.isStringLiteral(statement.moduleSpecifier) + ? statement.moduleSpecifier.text.replace(/"/g, "") + : "module"; + + // Create the require statement to extract values. + // local ____module = require("module") + const importUniqueName = lua.createIdentifier(createSafeName(path.basename(importPath))); + + let usingRequireStatement = false; + + // import defaultValue from "./module"; + // local defaultValue = __module.default + if (statement.importClause.name) { + if (shouldBeImported(context, statement.importClause)) { + const propertyName = createDefaultExportStringLiteral(statement.importClause.name); + const defaultImportAssignmentStatement = lua.createVariableDeclarationStatement( + transformIdentifier(context, statement.importClause.name), + lua.createTableIndexExpression(importUniqueName, propertyName), + statement.importClause.name + ); + + result.push(defaultImportAssignmentStatement); + usingRequireStatement = true; + } + } + + // import * as module from "./module"; + // local module = require("module") + if (statement.importClause.namedBindings && ts.isNamespaceImport(statement.importClause.namedBindings)) { + if (context.resolver.isReferencedAliasDeclaration(statement.importClause.namedBindings)) { + const requireStatement = lua.createVariableDeclarationStatement( + transformIdentifier(context, statement.importClause.namedBindings.name), + requireCall, + statement + ); + + result.push(requireStatement); + } + } + + // import { a, b, c } from "./module"; + // local a = __module.a + // local b = __module.b + // local c = __module.c + if (statement.importClause.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) { + const assignmentStatements = statement.importClause.namedBindings.elements + .filter(importSpecifier => shouldBeImported(context, importSpecifier)) + .map(importSpecifier => transformImportSpecifier(context, importSpecifier, importUniqueName)); + + if (assignmentStatements.length > 0) { + usingRequireStatement = true; + } + + result.push(...assignmentStatements); + } + + if (result.length === 0) { + return undefined; + } + + if (usingRequireStatement) { + result.unshift(lua.createVariableDeclarationStatement(importUniqueName, requireCall, statement)); + } + + scope.importStatements.push(...result); + return undefined; +}; + +export const transformExternalModuleReference: FunctionVisitor = (node, context) => + createModuleRequire(context, node.expression, node); + +export const transformImportEqualsDeclaration: FunctionVisitor = (node, context) => { + if ( + !context.resolver.isReferencedAliasDeclaration(node) && + (ts.isExternalModuleReference(node.moduleReference) || + ts.isExternalModule(context.sourceFile) || + !context.resolver.isTopLevelValueImportEqualsWithEntityName(node)) + ) { + return undefined; + } + + const name = transformIdentifier(context, node.name); + const expression = context.transformExpression(node.moduleReference); + return createHoistableVariableDeclarationStatement(context, name, expression, node); +}; + +export const transformImportExpression: FunctionVisitor = (node, context) => { + importLuaLibFeature(context, LuaLibFeature.Promise); + + const moduleRequire = + node.arguments.length > 0 ? createModuleRequire(context, node.arguments[0], node) : lua.createNilLiteral(); + return lua.createCallExpression(createStaticPromiseFunctionAccessor("resolve", node), [moduleRequire], node); +}; diff --git a/src/transformation/visitors/namespace.ts b/src/transformation/visitors/namespace.ts new file mode 100644 index 000000000..49a3f2289 --- /dev/null +++ b/src/transformation/visitors/namespace.ts @@ -0,0 +1,133 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { addExportToIdentifier, createExportedIdentifier, getIdentifierExportScope } from "../utils/export"; +import { + createHoistableVariableDeclarationStatement, + createLocalOrExportedOrGlobalDeclaration, +} from "../utils/lua-ast"; +import { createSafeName, isUnsafeName } from "../utils/safe-names"; +import { performHoisting, ScopeType } from "../utils/scope"; +import { getSymbolIdOfSymbol } from "../utils/symbols"; +import { transformIdentifier } from "./identifier"; + +export function createModuleLocalName(context: TransformationContext, module: ts.ModuleDeclaration): lua.Expression { + if (!ts.isSourceFile(module.parent) && ts.isModuleDeclaration(module.parent)) { + const parentDeclaration = createModuleLocalName(context, module.parent); + const name = createModuleLocalNameIdentifier(context, module); + return lua.createTableIndexExpression(parentDeclaration, lua.createStringLiteral(name.text), module.name); + } + + return createModuleLocalNameIdentifier(context, module); +} + +export function createModuleLocalNameIdentifier( + context: TransformationContext, + declaration: ts.ModuleDeclaration +): lua.Identifier { + const moduleSymbol = context.checker.getSymbolAtLocation(declaration.name); + if (moduleSymbol !== undefined && isUnsafeName(moduleSymbol.name, context.options)) { + return lua.createIdentifier( + createSafeName(declaration.name.text), + declaration.name, + moduleSymbol && getSymbolIdOfSymbol(context, moduleSymbol), + declaration.name.text + ); + } + + return transformIdentifier(context, declaration.name as ts.Identifier); +} + +// TODO: Do it based on transform result? +function moduleHasEmittedBody( + node: ts.ModuleDeclaration +): node is ts.ModuleDeclaration & { body: ts.ModuleBlock | ts.ModuleDeclaration } { + if (node.body) { + if (ts.isModuleBlock(node.body)) { + // Ignore if body has no emitted statements + return node.body.statements.some(s => !ts.isInterfaceDeclaration(s) && !ts.isTypeAliasDeclaration(s)); + } else if (ts.isModuleDeclaration(node.body)) { + return true; + } + } + + return false; +} + +export const transformModuleDeclaration: FunctionVisitor = (node, context) => { + const currentNamespace = context.currentNamespaces; + const result: lua.Statement[] = []; + + const symbol = context.checker.getSymbolAtLocation(node.name); + const hasExports = symbol !== undefined && context.checker.getExportsOfModule(symbol).length > 0; + const nameIdentifier = transformIdentifier(context, node.name as ts.Identifier); + const exportScope = getIdentifierExportScope(context, nameIdentifier); + + // Non-module namespace could be merged if: + // - is top level + // - is nested and exported + const isNonModuleMergeable = !context.isModule && (!currentNamespace || exportScope); + + // This is NOT the first declaration if: + // - declared as a module before this (ignore interfaces with same name) + // - declared as a class or function at all (TS requires these to be before module, unless module is empty) + const isFirstDeclaration = + symbol === undefined || + (!symbol.declarations?.some(d => ts.isClassLike(d) || ts.isFunctionDeclaration(d)) && + ts.getOriginalNode(node) === symbol.declarations?.find(ts.isModuleDeclaration)); + + if (isNonModuleMergeable) { + // 'local NS = NS or {}' or 'exportTable.NS = exportTable.NS or {}' + const localDeclaration = createLocalOrExportedOrGlobalDeclaration( + context, + nameIdentifier, + lua.createBinaryExpression( + addExportToIdentifier(context, nameIdentifier), + lua.createTableExpression(), + lua.SyntaxKind.OrOperator + ) + ); + + result.push(...localDeclaration); + } else if (isFirstDeclaration) { + // local NS = {} or exportTable.NS = {} + const localDeclaration = createLocalOrExportedOrGlobalDeclaration( + context, + nameIdentifier, + lua.createTableExpression() + ); + + result.push(...localDeclaration); + } + + if ((isNonModuleMergeable || isFirstDeclaration) && exportScope && hasExports && moduleHasEmittedBody(node)) { + // local NS = exportTable.NS + const localDeclaration = createHoistableVariableDeclarationStatement( + context, + createModuleLocalNameIdentifier(context, node), + createExportedIdentifier(context, nameIdentifier, exportScope) + ); + + result.push(localDeclaration); + } + + // Set current namespace for nested NS + // Keep previous namespace to reset after block transpilation + + context.currentNamespaces = node; + + // Transform moduleblock to block and visit it + if (moduleHasEmittedBody(node)) { + context.pushScope(ScopeType.Block, node); + const statements = performHoisting( + context, + context.transformStatements(ts.isModuleBlock(node.body) ? node.body.statements : node.body) + ); + context.popScope(); + result.push(lua.createDoStatement(statements)); + } + + context.currentNamespaces = currentNamespace; + + return result; +}; diff --git a/src/transformation/visitors/optional-chaining.ts b/src/transformation/visitors/optional-chaining.ts new file mode 100644 index 000000000..6d6deeb8d --- /dev/null +++ b/src/transformation/visitors/optional-chaining.ts @@ -0,0 +1,286 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { tempSymbolId, TransformationContext } from "../context"; +import { assert, assertNever } from "../../utils"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; +import { transformElementAccessExpressionWithCapture, transformPropertyAccessExpressionWithCapture } from "./access"; +import { shouldMoveToTemp } from "./expression-list"; +import { canBeFalsyWhenNotNull, expressionResultIsUsed } from "../utils/typescript"; +import { wrapInStatement } from "./expression-statement"; + +type NormalOptionalChain = ts.PropertyAccessChain | ts.ElementAccessChain | ts.CallChain; + +function skipNonNullChains(chain: ts.OptionalChain): NormalOptionalChain { + while (ts.isNonNullChain(chain)) { + chain = chain.expression as ts.OptionalChain; + } + return chain; +} + +function flattenChain(chain: ts.OptionalChain) { + chain = skipNonNullChains(chain); + const links: NormalOptionalChain[] = [chain]; + while (!chain.questionDotToken && !ts.isTaggedTemplateExpression(chain)) { + const nextLink: ts.Expression = chain.expression; + assert(ts.isOptionalChain(nextLink)); + chain = skipNonNullChains(nextLink); + links.unshift(chain); + } + return { expression: chain.expression, chain: links }; +} + +export interface ExpressionWithThisValue { + expression: lua.Expression; + thisValue?: lua.Expression; +} + +function transformExpressionWithThisValueCapture( + context: TransformationContext, + node: ts.Expression, + thisValueCapture: lua.Identifier +): ExpressionWithThisValue { + if (ts.isParenthesizedExpression(node)) { + return transformExpressionWithThisValueCapture(context, node.expression, thisValueCapture); + } + if (ts.isPropertyAccessExpression(node)) { + return transformPropertyAccessExpressionWithCapture(context, node, thisValueCapture); + } + if (ts.isElementAccessExpression(node)) { + return transformElementAccessExpressionWithCapture(context, node, thisValueCapture); + } + return { expression: context.transformExpression(node) }; +} + +// returns thisValueCapture exactly if a temp variable was used. +export function captureThisValue( + context: TransformationContext, + expression: lua.Expression, + thisValueCapture: lua.Identifier, + tsOriginal: ts.Node +): lua.Expression { + if (!shouldMoveToTemp(context, expression, tsOriginal)) { + return expression; + } + const tempAssignment = lua.createAssignmentStatement(thisValueCapture, expression, tsOriginal); + context.addPrecedingStatements(tempAssignment); + return thisValueCapture; +} + +export interface OptionalContinuation { + contextualCall?: lua.CallExpression; + usedIdentifiers: lua.Identifier[]; +} + +const optionalContinuations = new WeakMap(); + +// should be translated verbatim to lua +function createOptionalContinuationIdentifier(text: string, tsOriginal: ts.Expression): ts.Identifier { + const identifier = ts.factory.createIdentifier(text); + ts.setOriginalNode(identifier, tsOriginal); + optionalContinuations.set(identifier, { + usedIdentifiers: [], + }); + return identifier; +} + +export function isOptionalContinuation(node: ts.Node): boolean { + return ts.isIdentifier(node) && optionalContinuations.has(node); +} + +export function getOptionalContinuationData(identifier: ts.Identifier): OptionalContinuation | undefined { + return optionalContinuations.get(identifier); +} + +export function transformOptionalChain(context: TransformationContext, node: ts.OptionalChain): lua.Expression { + return transformOptionalChainWithCapture(context, node, undefined).expression; +} + +export function transformOptionalChainWithCapture( + context: TransformationContext, + tsNode: ts.OptionalChain, + thisValueCapture: lua.Identifier | undefined, + isDelete?: ts.DeleteExpression +): ExpressionWithThisValue { + const luaTempName = context.createTempName("opt"); + + const { expression: tsLeftExpression, chain } = flattenChain(tsNode); + + // build temp.b.c.d + const tsTemp = createOptionalContinuationIdentifier(luaTempName, tsLeftExpression); + let tsRightExpression: ts.Expression = tsTemp; + for (const link of chain) { + if (ts.isPropertyAccessExpression(link)) { + tsRightExpression = ts.factory.createPropertyAccessExpression(tsRightExpression, link.name); + } else if (ts.isElementAccessExpression(link)) { + tsRightExpression = ts.factory.createElementAccessExpression(tsRightExpression, link.argumentExpression); + } else if (ts.isCallExpression(link)) { + tsRightExpression = ts.factory.createCallExpression(tsRightExpression, undefined, link.arguments); + } else { + assertNever(link); + } + ts.setOriginalNode(tsRightExpression, link); + } + if (isDelete) { + tsRightExpression = ts.factory.createDeleteExpression(tsRightExpression); + ts.setOriginalNode(tsRightExpression, isDelete); + } + + // transform right expression first to check if thisValue capture is needed + // capture and return thisValue if requested from outside + let returnThisValue: lua.Expression | undefined; + const { precedingStatements: rightPrecedingStatements, result: rightExpression } = + transformInPrecedingStatementScope(context, () => { + if (!thisValueCapture) { + return context.transformExpression(tsRightExpression); + } + + const { expression: result, thisValue } = transformExpressionWithThisValueCapture( + context, + tsRightExpression, + thisValueCapture + ); + returnThisValue = thisValue; + return result; + }); + + // transform left expression, handle thisValue if needed by rightExpression + const thisValueCaptureName = context.createTempName("this"); + const leftThisValueTemp = lua.createIdentifier(thisValueCaptureName, undefined, tempSymbolId); + let capturedThisValue: lua.Expression | undefined; + + const optionalContinuationData = getOptionalContinuationData(tsTemp); + const rightContextualCall = optionalContinuationData?.contextualCall; + const { precedingStatements: leftPrecedingStatements, result: leftExpression } = transformInPrecedingStatementScope( + context, + () => { + let result: lua.Expression; + if (rightContextualCall) { + ({ expression: result, thisValue: capturedThisValue } = transformExpressionWithThisValueCapture( + context, + tsLeftExpression, + leftThisValueTemp + )); + } else { + result = context.transformExpression(tsLeftExpression); + } + return result; + } + ); + + // handle super calls by passing self as context + function getLeftMostChainItem(node: ts.Node): ts.Node { + if (ts.isPropertyAccessExpression(node)) { + return getLeftMostChainItem(node.expression); + } else { + return node; + } + } + if (getLeftMostChainItem(tsLeftExpression).kind === ts.SyntaxKind.SuperKeyword) { + capturedThisValue = lua.createIdentifier("self"); + } + + // handle context + if (rightContextualCall) { + if (capturedThisValue) { + rightContextualCall.params[0] = capturedThisValue; + if (capturedThisValue === leftThisValueTemp) { + context.addPrecedingStatements(lua.createVariableDeclarationStatement(leftThisValueTemp)); + } + } else { + if (context.isStrict) { + rightContextualCall.params[0] = lua.createNilLiteral(); + } else { + const identifier = lua.createIdentifier("_G"); + if (rightPrecedingStatements.length === 0) { + rightContextualCall.params[0] = identifier; + } else { + const tempContext = context.createTempNameForLuaExpression(identifier); + rightPrecedingStatements.unshift(lua.createVariableDeclarationStatement(tempContext, identifier)); + rightContextualCall.params[0] = tempContext; + } + } + } + } + + // evaluate optional chain + context.addPrecedingStatements(leftPrecedingStatements); + + // try use existing variable instead of creating new one, if possible + let leftIdentifier: lua.Identifier | undefined; + const usedLuaIdentifiers = optionalContinuationData?.usedIdentifiers; + const reuseLeftIdentifier = + usedLuaIdentifiers && + usedLuaIdentifiers.length > 0 && + lua.isIdentifier(leftExpression) && + (rightPrecedingStatements.length === 0 || !shouldMoveToTemp(context, leftExpression, tsLeftExpression)); + if (reuseLeftIdentifier) { + leftIdentifier = leftExpression; + for (const usedIdentifier of usedLuaIdentifiers) { + usedIdentifier.text = leftIdentifier.text; + } + } else { + leftIdentifier = lua.createIdentifier(luaTempName, undefined, tempSymbolId); + context.addPrecedingStatements(lua.createVariableDeclarationStatement(leftIdentifier, leftExpression)); + } + + if (!expressionResultIsUsed(tsNode) || isDelete) { + // if left ~= nil then + // + // + // end + + const innerExpression = wrapInStatement(rightExpression); + const innerStatements = rightPrecedingStatements; + if (innerExpression) innerStatements.push(innerExpression); + + context.addPrecedingStatements( + lua.createIfStatement( + lua.createBinaryExpression(leftIdentifier, lua.createNilLiteral(), lua.SyntaxKind.InequalityOperator), + lua.createBlock(innerStatements) + ) + ); + return { expression: lua.createNilLiteral(), thisValue: returnThisValue }; + } else if ( + rightPrecedingStatements.length === 0 && + !canBeFalsyWhenNotNull(context, context.checker.getTypeAtLocation(tsLeftExpression)) + ) { + // return a && a.b + return { + expression: lua.createBinaryExpression(leftIdentifier, rightExpression, lua.SyntaxKind.AndOperator, tsNode), + thisValue: returnThisValue, + }; + } else { + let resultIdentifier: lua.Identifier; + if (!reuseLeftIdentifier) { + // reuse temp variable for output + resultIdentifier = leftIdentifier; + } else { + resultIdentifier = lua.createIdentifier(context.createTempName("opt_result"), undefined, tempSymbolId); + context.addPrecedingStatements(lua.createVariableDeclarationStatement(resultIdentifier)); + } + // if left ~= nil then + // + // result = + // end + // return result + context.addPrecedingStatements( + lua.createIfStatement( + lua.createBinaryExpression(leftIdentifier, lua.createNilLiteral(), lua.SyntaxKind.InequalityOperator), + lua.createBlock([ + ...rightPrecedingStatements, + lua.createAssignmentStatement(resultIdentifier, rightExpression), + ]) + ) + ); + return { expression: resultIdentifier, thisValue: returnThisValue }; + } +} + +export function transformOptionalDeleteExpression( + context: TransformationContext, + node: ts.DeleteExpression, + innerExpression: ts.OptionalChain +) { + transformOptionalChainWithCapture(context, innerExpression, undefined, node); + return lua.createBooleanLiteral(true, node); +} diff --git a/src/transformation/visitors/return.ts b/src/transformation/visitors/return.ts new file mode 100644 index 000000000..14d785166 --- /dev/null +++ b/src/transformation/visitors/return.ts @@ -0,0 +1,122 @@ +import * as ts from "typescript"; +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 { transformArguments } from "./call"; +import { + returnsMultiType, + shouldMultiReturnCallBeWrapped, + isMultiFunctionCall, + isMultiReturnType, + isInMultiReturnFunction, + canBeMultiReturnType, +} from "./language-extensions/multi"; +import { invalidMultiFunctionReturnType } from "../utils/diagnostics"; +import { isInAsyncFunction } from "../utils/typescript"; + +function transformExpressionsInReturn( + context: TransformationContext, + node: ts.Expression, + insideTryCatch: boolean +): lua.Expression[] { + const expressionType = context.checker.getTypeAtLocation(node); + + // skip type assertions + // don't skip parenthesis as it may arise confusion with lua behavior (where parenthesis are significant) + const innerNode = ts.skipOuterExpressions(node, ts.OuterExpressionKinds.Assertions); + + if (ts.isCallExpression(innerNode)) { + // $multi(...) + if (isMultiFunctionCall(context, innerNode)) { + // Don't allow $multi to be implicitly cast to something other than LuaMultiReturn + const type = context.checker.getContextualType(node); + if (type && !canBeMultiReturnType(type)) { + 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))]; + } + } else if (isInMultiReturnFunction(context, innerNode) && isMultiReturnType(expressionType)) { + // Unpack objects typed as LuaMultiReturn + return [createUnpackCall(context, context.transformExpression(innerNode), innerNode)]; + } + + return [context.transformExpression(node)]; +} + +export function transformExpressionBodyToReturnStatement( + context: TransformationContext, + node: ts.Expression +): lua.Statement { + const expressions = transformExpressionsInReturn(context, node, false); + return createReturnStatement(context, expressions, node); +} + +export const transformReturnStatement: FunctionVisitor = (statement, context) => { + let results: lua.Expression[]; + + if (statement.expression) { + const expressionType = context.checker.getTypeAtLocation(statement.expression); + const returnType = context.checker.getContextualType(statement.expression); + if (returnType) { + validateAssignment(context, statement, expressionType, returnType); + } + + results = transformExpressionsInReturn(context, statement.expression, isInTryCatch(context)); + } else { + // Empty return + results = []; + } + + return createReturnStatement(context, results, statement); +}; + +export function createReturnStatement( + context: TransformationContext, + values: lua.Expression[], + node: ts.Node +): lua.ReturnStatement { + if (isInAsyncFunction(node)) { + return lua.createReturnStatement([ + lua.createCallExpression(lua.createIdentifier("____awaiter_resolve"), [lua.createNilLiteral(), ...values]), + ]); + } + + if (isInTryCatch(context)) { + // Bubble up explicit return flag and check if we're inside a try/catch block + values = [lua.createBooleanLiteral(true), ...values]; + } + + return lua.createReturnStatement(values, node); +} + +function isInTryCatch(context: TransformationContext): boolean { + // Check if context is in a try or catch + let insideTryCatch = false; + for (const scope of walkScopesUp(context)) { + scope.functionReturned = true; + + if (scope.type === ScopeType.Function) { + break; + } + + insideTryCatch = insideTryCatch || scope.type === ScopeType.Try || scope.type === ScopeType.Catch; + } + + return insideTryCatch; +} diff --git a/src/transformation/visitors/sourceFile.ts b/src/transformation/visitors/sourceFile.ts new file mode 100644 index 000000000..2fe860f53 --- /dev/null +++ b/src/transformation/visitors/sourceFile.ts @@ -0,0 +1,50 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { assert } from "../../utils"; +import { FunctionVisitor } from "../context"; +import { createExportsIdentifier } from "../utils/lua-ast"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; +import { performHoisting, ScopeType } from "../utils/scope"; +import { hasExportEquals } from "../utils/typescript"; + +export const transformSourceFileNode: FunctionVisitor = (node, context) => { + let statements: lua.Statement[] = []; + if (node.flags & ts.NodeFlags.JsonFile) { + const [statement] = node.statements; + if (statement) { + assert(ts.isExpressionStatement(statement)); + const { precedingStatements, result: expression } = transformInPrecedingStatementScope(context, () => + context.transformExpression(statement.expression) + ); + statements.push(...precedingStatements); + statements.push(lua.createReturnStatement([expression])); + } else { + const errorCall = lua.createCallExpression(lua.createIdentifier("error"), [ + lua.createStringLiteral("Unexpected end of JSON input"), + ]); + + statements.push(lua.createExpressionStatement(errorCall)); + } + } else { + context.pushScope(ScopeType.File, node); + + statements = performHoisting(context, context.transformStatements(node.statements)); + context.popScope(); + + if (context.isModule) { + // If export equals was not used. Create the exports table. + // local ____exports = {} + if (!hasExportEquals(node)) { + statements.unshift( + lua.createVariableDeclarationStatement(createExportsIdentifier(), lua.createTableExpression()) + ); + } + + // return ____exports + statements.push(lua.createReturnStatement([createExportsIdentifier()])); + } + } + + const trivia = node.getFullText().match(/^#!.*\r?\n/)?.[0] ?? ""; + return lua.createFile(statements, context.usedLuaLibFeatures, trivia, node); +}; diff --git a/src/transformation/visitors/spread.ts b/src/transformation/visitors/spread.ts new file mode 100644 index 000000000..b10a0ae6e --- /dev/null +++ b/src/transformation/visitors/spread.ts @@ -0,0 +1,99 @@ +import * as ts from "typescript"; +import { LuaTarget } from "../../CompilerOptions"; +import * as lua from "../../LuaAST"; +import { assertNever } from "../../utils"; +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, ScopeType } from "../utils/scope"; +import { findFirstNonOuterParent, isAlwaysArrayType } from "../utils/typescript"; +import { isMultiReturnCall } from "./language-extensions/multi"; +import { isGlobalVarargConstant } from "./language-extensions/vararg"; + +export function isOptimizedVarArgSpread(context: TransformationContext, symbol: ts.Symbol, identifier: ts.Identifier) { + if (!ts.isSpreadElement(findFirstNonOuterParent(identifier))) { + return false; + } + + // Walk up, stopping at any scope types which could stop optimization + const scope = findScope(context, ScopeType.Function | ScopeType.Try | ScopeType.Catch | ScopeType.File); + if (!scope) { + return; + } + + // $vararg global constant + if (isGlobalVarargConstant(context, symbol, scope)) { + return true; + } + + // Scope must be a function scope associated with a real ts function + if (!ts.isFunctionLike(scope.node)) { + return false; + } + + // Scope cannot be an async function + if ( + ts.canHaveModifiers(scope.node) && + ts.getModifiers(scope.node)?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) + ) { + return false; + } + + // Identifier must be a vararg in the local function scope's parameters + const isSpreadParameter = (p: ts.ParameterDeclaration) => + p.dotDotDotToken && ts.isIdentifier(p.name) && context.checker.getSymbolAtLocation(p.name) === symbol; + if (!scope.node.parameters.some(isSpreadParameter)) { + return false; + } + + // De-optimize if already referenced outside of a spread, as the array may have been modified + if (hasReferencedSymbol(context, scope, symbol)) { + return false; + } + + // De-optimize if a function is being hoisted from below to above, as it may have modified the array + if (hasReferencedUndefinedLocalFunction(context, scope)) { + return false; + } + return true; +} + +// TODO: Currently it's also used as an array member +export const transformSpreadElement: FunctionVisitor = (node, context) => { + const tsInnerExpression = ts.skipOuterExpressions(node.expression); + if (ts.isIdentifier(tsInnerExpression)) { + const symbol = context.checker.getSymbolAtLocation(tsInnerExpression); + if (symbol && isOptimizedVarArgSpread(context, symbol, tsInnerExpression)) { + return context.luaTarget === LuaTarget.Lua50 + ? createUnpackCall(context, lua.createArgLiteral(), node) + : lua.createDotsLiteral(node); + } + } + + const innerExpression = context.transformExpression(node.expression); + if (isMultiReturnCall(context, tsInnerExpression)) return innerExpression; + + const iterableExtensionType = getIterableExtensionKindForNode(context, node.expression); + if (iterableExtensionType) { + if (iterableExtensionType === IterableExtensionKind.Iterable) { + return transformLuaLibFunction(context, LuaLibFeature.LuaIteratorSpread, node, innerExpression); + } else if (iterableExtensionType === IterableExtensionKind.Pairs) { + const objectEntries = transformLuaLibFunction(context, LuaLibFeature.ObjectEntries, node, innerExpression); + return createUnpackCall(context, objectEntries, node); + } else if (iterableExtensionType === IterableExtensionKind.PairsKey) { + const objectKeys = transformLuaLibFunction(context, LuaLibFeature.ObjectKeys, node, innerExpression); + return createUnpackCall(context, objectKeys, node); + } else { + assertNever(iterableExtensionType); + } + } + + const type = context.checker.getTypeAtLocation(node.expression); // not ts-inner expression, in case of casts + if (isAlwaysArrayType(context, type)) { + // All union members must be arrays to be able to shortcut to unpack call + return createUnpackCall(context, innerExpression, node); + } + + return transformLuaLibFunction(context, LuaLibFeature.Spread, node, innerExpression); +}; diff --git a/src/transformation/visitors/switch.ts b/src/transformation/visitors/switch.ts new file mode 100644 index 000000000..cee8b7f57 --- /dev/null +++ b/src/transformation/visitors/switch.ts @@ -0,0 +1,269 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { transformInPrecedingStatementScope, WithPrecedingStatements } from "../utils/preceding-statements"; +import { ScopeType, separateHoistedStatements } from "../utils/scope"; +import { createShortCircuitBinaryExpressionPrecedingStatements } from "./binary-expression"; + +const containsBreakOrReturn = (nodes: Iterable): boolean => { + for (const s of nodes) { + if (ts.isBreakStatement(s) || ts.isReturnStatement(s)) { + return true; + } else if (ts.isBlock(s) && containsBreakOrReturn(s.statements)) { + return true; + } else if (s.kind === ts.SyntaxKind.SyntaxList) { + // We cannot use getChildren() because that breaks when using synthetic nodes from transformers + // So get children the long way + const children: ts.Node[] = []; + ts.forEachChild(s, c => children.push(c)); + if (containsBreakOrReturn(children)) { + return true; + } + } + } + + return false; +}; + +const createOrExpression = ( + context: TransformationContext, + left: lua.Expression, + right: lua.Expression, + rightPrecedingStatements: lua.Statement[] +): WithPrecedingStatements => { + if (rightPrecedingStatements.length > 0) { + return createShortCircuitBinaryExpressionPrecedingStatements( + context, + left, + right, + rightPrecedingStatements, + ts.SyntaxKind.BarBarToken + ); + } else { + return { + precedingStatements: rightPrecedingStatements, + result: lua.createBinaryExpression(left, right, lua.SyntaxKind.OrOperator), + }; + } +}; + +const coalesceCondition = ( + condition: lua.Expression | undefined, + conditionPrecedingStatements: lua.Statement[], + switchVariable: lua.Identifier, + expression: ts.Expression, + context: TransformationContext +): WithPrecedingStatements => { + const { precedingStatements, result: transformedExpression } = transformInPrecedingStatementScope(context, () => + context.transformExpression(expression) + ); + + // Coalesce skipped statements + const comparison = lua.createBinaryExpression( + switchVariable, + transformedExpression, + lua.SyntaxKind.EqualityOperator + ); + if (condition) { + return createOrExpression(context, condition, comparison, precedingStatements); + } + + // Next condition + return { precedingStatements: [...conditionPrecedingStatements, ...precedingStatements], result: comparison }; +}; + +export const transformSwitchStatement: FunctionVisitor = (statement, context) => { + 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}`; + const conditionName = `____cond${scope.id}`; + const switchVariable = lua.createIdentifier(switchName); + const conditionVariable = lua.createIdentifier(conditionName); + + // If the switch only has a default clause, wrap it in a single do. + // Otherwise, we need to generate a set of if statements to emulate the switch. + const statements: lua.Statement[] = []; + const hoistedStatements: lua.Statement[] = []; + const hoistedIdentifiers: lua.Identifier[] = []; + const clauses = statement.caseBlock.clauses; + if (clauses.length === 1 && ts.isDefaultClause(clauses[0])) { + const defaultClause = clauses[0].statements; + if (defaultClause.length) { + const { + statements: defaultStatements, + hoistedStatements: defaultHoistedStatements, + hoistedIdentifiers: defaultHoistedIdentifiers, + } = separateHoistedStatements(context, context.transformStatements(defaultClause)); + hoistedStatements.push(...defaultHoistedStatements); + hoistedIdentifiers.push(...defaultHoistedIdentifiers); + statements.push(lua.createDoStatement(defaultStatements)); + } + } else { + // Build up the condition for each if statement + let defaultTransformed = false; + let isInitialCondition = true; + let condition: lua.Expression | undefined = undefined; + let conditionPrecedingStatements: lua.Statement[] = []; + for (let i = 0; i < clauses.length; i++) { + const clause = clauses[i]; + const previousClause: ts.CaseOrDefaultClause | undefined = clauses[i - 1]; + + // Skip redundant default clauses, will be handled in final default case + if (i === 0 && ts.isDefaultClause(clause)) continue; + if (ts.isDefaultClause(clause) && previousClause && containsBreakOrReturn(previousClause.statements)) { + continue; + } + + // Compute the condition for the if statement + if (!ts.isDefaultClause(clause)) { + const { precedingStatements, result } = coalesceCondition( + condition, + conditionPrecedingStatements, + switchVariable, + clause.expression, + context + ); + conditionPrecedingStatements = precedingStatements; + condition = result; + + // Skip empty clauses unless final clause (i.e side-effects) + if (i !== clauses.length - 1 && clause.statements.length === 0) continue; + + // Declare or assign condition variable + if (isInitialCondition) { + statements.push( + ...conditionPrecedingStatements, + lua.createVariableDeclarationStatement(conditionVariable, condition) + ); + } else { + const { precedingStatements, result } = createOrExpression( + context, + conditionVariable, + condition, + conditionPrecedingStatements + ); + conditionPrecedingStatements = precedingStatements; + condition = result; + + statements.push( + ...conditionPrecedingStatements, + lua.createAssignmentStatement(conditionVariable, condition) + ); + } + isInitialCondition = false; + } else { + // If the default is proceeded by empty clauses and will be emitted we may need to initialize the condition + if (isInitialCondition) { + statements.push( + ...conditionPrecedingStatements, + lua.createVariableDeclarationStatement( + conditionVariable, + condition ?? lua.createBooleanLiteral(false) + ) + ); + + // Clear condition ot ensure it is not evaluated twice + condition = undefined; + conditionPrecedingStatements = []; + isInitialCondition = false; + } + + // Allow default to fallthrough to final default clause + if (i === clauses.length - 1) { + // Evaluate the final condition that we may be skipping + if (condition) { + const { precedingStatements, result } = createOrExpression( + context, + conditionVariable, + condition, + conditionPrecedingStatements + ); + conditionPrecedingStatements = precedingStatements; + condition = result; + statements.push( + ...conditionPrecedingStatements, + lua.createAssignmentStatement(conditionVariable, condition) + ); + } + continue; + } + } + + // Transform the clause and append the final break statement if necessary + const { + statements: clauseStatements, + hoistedStatements: clauseHoistedStatements, + hoistedIdentifiers: clauseHoistedIdentifiers, + } = separateHoistedStatements(context, context.transformStatements(clause.statements)); + if (i === clauses.length - 1 && !containsBreakOrReturn(clause.statements)) { + clauseStatements.push(lua.createBreakStatement()); + } + hoistedStatements.push(...clauseHoistedStatements); + hoistedIdentifiers.push(...clauseHoistedIdentifiers); + + // Remember that we transformed default clause so we don't duplicate hoisted statements later + if (ts.isDefaultClause(clause)) { + defaultTransformed = true; + } + + // Push if statement for case + statements.push(lua.createIfStatement(conditionVariable, lua.createBlock(clauseStatements))); + + // Clear condition for next clause + condition = undefined; + conditionPrecedingStatements = []; + } + + // If no conditions above match, we need to create the final default case code-path, + // as we only handle fallthrough into defaults in the previous if statement chain + const start = clauses.findIndex(c => ts.isDefaultClause(c)); + if (start >= 0) { + // Find the last clause that we can fallthrough to + const end = clauses.findIndex( + (clause, index) => index >= start && containsBreakOrReturn(clause.statements) + ); + + const { + statements: defaultStatements, + hoistedStatements: defaultHoistedStatements, + hoistedIdentifiers: defaultHoistedIdentifiers, + } = separateHoistedStatements(context, context.transformStatements(clauses[start].statements)); + + // Only push hoisted statements if this is the first time we're transforming the default clause + if (!defaultTransformed) { + hoistedStatements.push(...defaultHoistedStatements); + hoistedIdentifiers.push(...defaultHoistedIdentifiers); + } + + // Combine the fallthrough statements + for (const clause of clauses.slice(start + 1, end >= 0 ? end + 1 : undefined)) { + let statements = context.transformStatements(clause.statements); + // Drop hoisted statements as they were already added when clauses were initially transformed above + ({ statements } = separateHoistedStatements(context, statements)); + defaultStatements.push(...statements); + } + + // Add the default clause if it has any statements + // The switch will always break on the final clause and skip execution if valid to do so + if (defaultStatements.length) { + statements.push(lua.createDoStatement(defaultStatements)); + } + } + } + + // Hoist the variable, function, and import statements to the top of the switch + statements.unshift(...hoistedStatements); + if (hoistedIdentifiers.length > 0) { + statements.unshift(lua.createVariableDeclarationStatement(hoistedIdentifiers)); + } + + context.popScope(); + + // Add the switch expression after hoisting + const expression = context.transformExpression(statement.expression); + statements.unshift(lua.createVariableDeclarationStatement(switchVariable, expression)); + + // Wrap the statements in a repeat until true statement to facilitate dynamic break/returns + return lua.createRepeatStatement(lua.createBlock(statements), lua.createBooleanLiteral(true)); +}; diff --git a/src/transformation/visitors/template.ts b/src/transformation/visitors/template.ts new file mode 100644 index 000000000..291c703d5 --- /dev/null +++ b/src/transformation/visitors/template.ts @@ -0,0 +1,102 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor } from "../context"; +import { ContextType, getCallContextType } from "../utils/function-context"; +import { wrapInToStringForConcat } from "../utils/lua-ast"; +import { isStringType } from "../utils/typescript"; +import { transformArguments, transformContextualCallExpression } from "./call"; +import { transformOrderedExpressions } from "./expression-list"; + +// TODO: Source positions +function getRawLiteral(node: ts.LiteralLikeNode): string { + let text = node.getText(); + const isLast = + node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateTail; + text = text.substring(1, text.length - (isLast ? 1 : 2)); + text = text.replace(/\r\n?/g, "\n"); + return text; +} + +export const transformTemplateExpression: FunctionVisitor = (node, context) => { + const parts: lua.Expression[] = []; + + const head = node.head.text; + if (head.length > 0) { + parts.push(lua.createStringLiteral(head, node.head)); + } + + const transformedExpressions = transformOrderedExpressions( + context, + node.templateSpans.map(s => s.expression) + ); + for (let i = 0; i < node.templateSpans.length; ++i) { + const span = node.templateSpans[i]; + const expression = transformedExpressions[i]; + const spanType = context.checker.getTypeAtLocation(span.expression); + if (isStringType(context, spanType)) { + parts.push(expression); + } else { + parts.push(wrapInToStringForConcat(expression)); + } + + const text = span.literal.text; + if (text.length > 0) { + parts.push(lua.createStringLiteral(text, span.literal)); + } + } + + return parts.reduce((prev, current) => lua.createBinaryExpression(prev, current, lua.SyntaxKind.ConcatOperator)); +}; + +export const transformTaggedTemplateExpression: FunctionVisitor = ( + expression, + context +) => { + const strings: string[] = []; + const rawStrings: string[] = []; + const expressions: ts.Expression[] = []; + + if (ts.isTemplateExpression(expression.template)) { + // Expressions are in the string. + strings.push(expression.template.head.text); + rawStrings.push(getRawLiteral(expression.template.head)); + strings.push(...expression.template.templateSpans.map(span => span.literal.text)); + rawStrings.push(...expression.template.templateSpans.map(span => getRawLiteral(span.literal))); + expressions.push(...expression.template.templateSpans.map(span => span.expression)); + } else { + // No expressions are in the string. + strings.push(expression.template.text); + rawStrings.push(getRawLiteral(expression.template)); + } + + // Construct table with strings and literal strings + + const rawStringsArray = ts.factory.createArrayLiteralExpression( + rawStrings.map(text => ts.factory.createStringLiteral(text)) + ); + + const stringObject = ts.factory.createObjectLiteralExpression([ + ...strings.map((partialString, i) => + ts.factory.createPropertyAssignment( + ts.factory.createNumericLiteral(i + 1), + ts.factory.createStringLiteral(partialString) + ) + ), + ts.factory.createPropertyAssignment("raw", rawStringsArray), + ]); + + expressions.unshift(stringObject); + + // Evaluate if there is a self parameter to be used. + const useSelfParameter = getCallContextType(context, expression) !== ContextType.Void; + + if (useSelfParameter) { + return transformContextualCallExpression(context, expression, expressions); + } + + // Argument evaluation. + const callArguments = transformArguments(context, expressions); + + const leftHandSideExpression = context.transformExpression(expression.tag); + return lua.createCallExpression(leftHandSideExpression, callArguments); +}; diff --git a/src/transformation/visitors/typeof.ts b/src/transformation/visitors/typeof.ts new file mode 100644 index 000000000..6b70946a3 --- /dev/null +++ b/src/transformation/visitors/typeof.ts @@ -0,0 +1,60 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { LuaLibFeature } from "../../LuaLib"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { transformLuaLibFunction } from "../utils/lualib"; +import { transformBinaryOperation } from "./binary-expression"; + +export const transformTypeOfExpression: FunctionVisitor = (node, context) => { + const innerExpression = context.transformExpression(node.expression); + return transformLuaLibFunction(context, LuaLibFeature.TypeOf, node, innerExpression); +}; + +export function transformTypeOfBinaryExpression( + context: TransformationContext, + node: ts.BinaryExpression +): lua.Expression | undefined { + const operator = node.operatorToken.kind; + if ( + operator !== ts.SyntaxKind.EqualsEqualsToken && + operator !== ts.SyntaxKind.EqualsEqualsEqualsToken && + operator !== ts.SyntaxKind.ExclamationEqualsToken && + operator !== ts.SyntaxKind.ExclamationEqualsEqualsToken + ) { + return; + } + + let literalExpression: ts.Expression; + let typeOfExpression: ts.TypeOfExpression; + if (ts.isTypeOfExpression(node.left)) { + typeOfExpression = node.left; + literalExpression = node.right; + } else if (ts.isTypeOfExpression(node.right)) { + typeOfExpression = node.right; + literalExpression = node.left; + } else { + return; + } + + const comparedExpression = context.transformExpression(literalExpression); + if (!lua.isStringLiteral(comparedExpression)) return; + + if (comparedExpression.value === "object") { + comparedExpression.value = "table"; + } else if (comparedExpression.value === "undefined") { + comparedExpression.value = "nil"; + } + + const innerExpression = context.transformExpression(typeOfExpression.expression); + const typeCall = lua.createCallExpression(lua.createIdentifier("type"), [innerExpression], typeOfExpression); + const { precedingStatements, result } = transformBinaryOperation( + context, + typeCall, + comparedExpression, + [], + operator, + node + ); + context.addPrecedingStatements(precedingStatements); + return result; +} diff --git a/src/transformation/visitors/typescript.ts b/src/transformation/visitors/typescript.ts new file mode 100644 index 000000000..0d9f8a8c1 --- /dev/null +++ b/src/transformation/visitors/typescript.ts @@ -0,0 +1,28 @@ +import * as ts from "typescript"; +import { FunctionVisitor, Visitors } from "../context"; +import { validateAssignment } from "../utils/assignment-validation"; + +const transformAssertionExpression: FunctionVisitor = (expression, context) => { + if (!ts.isConstTypeReference(expression.type)) { + validateAssignment( + context, + expression, + context.checker.getTypeAtLocation(expression.expression), + context.checker.getTypeAtLocation(expression.type) + ); + } + + return context.transformExpression(expression.expression); +}; + +export const typescriptVisitors: Visitors = { + [ts.SyntaxKind.TypeAliasDeclaration]: () => undefined, + [ts.SyntaxKind.InterfaceDeclaration]: () => undefined, + + [ts.SyntaxKind.NonNullExpression]: (node, context) => context.transformExpression(node.expression), + [ts.SyntaxKind.ExpressionWithTypeArguments]: (node, context) => context.transformExpression(node.expression), + [ts.SyntaxKind.SatisfiesExpression]: (node, context) => context.transformExpression(node.expression), + [ts.SyntaxKind.AsExpression]: transformAssertionExpression, + [ts.SyntaxKind.TypeAssertionExpression]: transformAssertionExpression, + [ts.SyntaxKind.NotEmittedStatement]: () => undefined, +}; diff --git a/src/transformation/visitors/unary-expression.ts b/src/transformation/visitors/unary-expression.ts new file mode 100644 index 000000000..88b512757 --- /dev/null +++ b/src/transformation/visitors/unary-expression.ts @@ -0,0 +1,137 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { assertNever } from "../../utils"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { transformUnaryBitOperation } from "./binary-expression/bit"; +import { + transformCompoundAssignmentExpression, + transformCompoundAssignmentStatement, +} from "./binary-expression/compound"; +import { isNumberType } from "../utils/typescript"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; + +export function transformUnaryExpressionStatement( + context: TransformationContext, + node: ts.ExpressionStatement +): lua.Statement[] | undefined { + const expression = ts.isExpressionStatement(node) ? node.expression : node; + if ( + ts.isPrefixUnaryExpression(expression) && + (expression.operator === ts.SyntaxKind.PlusPlusToken || expression.operator === ts.SyntaxKind.MinusMinusToken) + ) { + // ++i, --i + const replacementOperator = + expression.operator === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken; + + return transformCompoundAssignmentStatement( + context, + expression, + expression.operand, + ts.factory.createNumericLiteral(1), + replacementOperator + ); + } else if (ts.isPostfixUnaryExpression(expression)) { + // i++, i-- + const replacementOperator = + expression.operator === ts.SyntaxKind.PlusPlusToken ? ts.SyntaxKind.PlusToken : ts.SyntaxKind.MinusToken; + + return transformCompoundAssignmentStatement( + context, + expression, + expression.operand, + ts.factory.createNumericLiteral(1), + replacementOperator + ); + } +} + +export const transformPostfixUnaryExpression: FunctionVisitor = (expression, context) => { + switch (expression.operator) { + case ts.SyntaxKind.PlusPlusToken: + return transformCompoundAssignmentExpression( + context, + expression, + expression.operand, + ts.factory.createNumericLiteral(1), + ts.SyntaxKind.PlusToken, + true + ); + + case ts.SyntaxKind.MinusMinusToken: + return transformCompoundAssignmentExpression( + context, + expression, + expression.operand, + ts.factory.createNumericLiteral(1), + ts.SyntaxKind.MinusToken, + true + ); + + default: + assertNever(expression.operator); + } +}; + +export const transformPrefixUnaryExpression: FunctionVisitor = (expression, context) => { + switch (expression.operator) { + case ts.SyntaxKind.PlusPlusToken: + return transformCompoundAssignmentExpression( + context, + expression, + expression.operand, + ts.factory.createNumericLiteral(1), + ts.SyntaxKind.PlusToken, + false + ); + + case ts.SyntaxKind.MinusMinusToken: + return transformCompoundAssignmentExpression( + context, + expression, + expression.operand, + ts.factory.createNumericLiteral(1), + ts.SyntaxKind.MinusToken, + false + ); + + case ts.SyntaxKind.PlusToken: { + const operand = context.transformExpression(expression.operand); + const type = context.checker.getTypeAtLocation(expression.operand); + if (isNumberType(context, type)) { + return operand; + } else { + return transformLuaLibFunction(context, LuaLibFeature.Number, expression, operand); + } + } + case ts.SyntaxKind.MinusToken: { + const operand = context.transformExpression(expression.operand); + const type = context.checker.getTypeAtLocation(expression.operand); + if (isNumberType(context, type)) { + return lua.createUnaryExpression(operand, lua.SyntaxKind.NegationOperator); + } else { + return transformLuaLibFunction( + context, + LuaLibFeature.Number, + expression, + lua.createUnaryExpression(operand, lua.SyntaxKind.NegationOperator) + ); + } + } + case ts.SyntaxKind.ExclamationToken: + return lua.createUnaryExpression( + context.transformExpression(expression.operand), + lua.SyntaxKind.NotOperator + ); + + case ts.SyntaxKind.TildeToken: + return transformUnaryBitOperation( + context, + expression, + context.transformExpression(expression.operand), + lua.SyntaxKind.BitwiseNotOperator + ); + + default: + assertNever(expression.operator); + } +}; diff --git a/src/transformation/visitors/variable-declaration.ts b/src/transformation/visitors/variable-declaration.ts new file mode 100644 index 000000000..f15ed0115 --- /dev/null +++ b/src/transformation/visitors/variable-declaration.ts @@ -0,0 +1,296 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { assert, assertNever } from "../../utils"; +import { FunctionVisitor, TransformationContext } from "../context"; +import { validateAssignment } from "../utils/assignment-validation"; +import { unsupportedVarDeclaration } from "../utils/diagnostics"; +import { addExportToIdentifier } from "../utils/export"; +import { createBoundedUnpackCall, createLocalOrExportedOrGlobalDeclaration, wrapInTable } from "../utils/lua-ast"; +import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; +import { transformInPrecedingStatementScope } from "../utils/preceding-statements"; +import { createCallableTable, isFunctionTypeWithProperties } from "./function"; +import { transformIdentifier } from "./identifier"; +import { isMultiReturnCall } from "./language-extensions/multi"; +import { transformPropertyName } from "./literal"; +import { moveToPrecedingTemp } from "./expression-list"; + +export function transformArrayBindingElement( + context: TransformationContext, + name: ts.ArrayBindingElement +): lua.Identifier { + if (ts.isOmittedExpression(name)) { + return lua.createAnonymousIdentifier(name); + } else if (ts.isIdentifier(name)) { + return transformIdentifier(context, name); + } else if (ts.isBindingElement(name)) { + if (ts.isIdentifier(name.name)) { + return transformIdentifier(context, name.name); + } else { + // ts.isBindingPattern(name.name) + const tempName = context.createTempNameForNode(name.name); + context.addPrecedingStatements(transformBindingPattern(context, name.name, tempName)); + return tempName; + } + } else { + assertNever(name); + } +} + +export function transformBindingPattern( + context: TransformationContext, + pattern: ts.BindingPattern, + table: lua.Expression, + propertyAccessStack: ts.PropertyName[] = [] +): lua.Statement[] { + const result: lua.Statement[] = []; + + for (const [index, element] of pattern.elements.entries()) { + if (ts.isOmittedExpression(element)) continue; + + if (ts.isArrayBindingPattern(element.name) || ts.isObjectBindingPattern(element.name)) { + // nested binding pattern + const propertyName = ts.isObjectBindingPattern(pattern) + ? element.propertyName + : ts.factory.createNumericLiteral(String(index + 1)); + + if (propertyName !== undefined) { + propertyAccessStack.push(propertyName); + } + + result.push(...transformBindingPattern(context, element.name, table, propertyAccessStack)); + continue; + } + + // Build the path to the table + const tableExpression = propertyAccessStack.reduce( + (path, property) => lua.createTableIndexExpression(path, transformPropertyName(context, property)), + table + ); + + // The identifier of the new variable + const variableName = transformIdentifier(context, element.name); + // The field to extract + const elementName = element.propertyName ?? element.name; + const { precedingStatements, result: propertyName } = transformInPrecedingStatementScope(context, () => + transformPropertyName(context, elementName) + ); + result.push(...precedingStatements); // Keep property's preceding statements in order + + let expression: lua.Expression; + if (element.dotDotDotToken) { + if (index !== pattern.elements.length - 1) { + // TypeScript error + continue; + } + + if (ts.isObjectBindingPattern(pattern)) { + const excludedProperties: ts.Identifier[] = []; + + for (const element of pattern.elements) { + // const { ...x } = ...; + // ~~~~ + if (element.dotDotDotToken) continue; + + // const { x } = ...; + // ~ + if (ts.isIdentifier(element.name) && !element.propertyName) { + excludedProperties.push(element.name); + } + + // const { x: ... } = ...; + // ~~~~~~ + if (element.propertyName && element.name && ts.isIdentifier(element.propertyName)) { + excludedProperties.push(element.propertyName); + } + } + + const excludedPropertiesTable = excludedProperties.map(e => + lua.createTableFieldExpression(lua.createBooleanLiteral(true), lua.createStringLiteral(e.text, e)) + ); + + expression = transformLuaLibFunction( + context, + LuaLibFeature.ObjectRest, + undefined, + tableExpression, + lua.createTableExpression(excludedPropertiesTable) + ); + } else { + expression = transformLuaLibFunction( + context, + LuaLibFeature.ArraySlice, + undefined, + tableExpression, + lua.createNumericLiteral(index) + ); + } + } else { + expression = lua.createTableIndexExpression( + tableExpression, + ts.isObjectBindingPattern(pattern) ? propertyName : lua.createNumericLiteral(index + 1) + ); + } + + result.push(...createLocalOrExportedOrGlobalDeclaration(context, variableName, expression)); + if (element.initializer) { + const identifier = addExportToIdentifier(context, variableName); + const tsInitializer = element.initializer; + const { precedingStatements: initializerPrecedingStatements, result: initializer } = + transformInPrecedingStatementScope(context, () => context.transformExpression(tsInitializer)); + result.push( + lua.createIfStatement( + lua.createBinaryExpression(identifier, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator), + lua.createBlock([ + ...initializerPrecedingStatements, + lua.createAssignmentStatement(identifier, initializer), + ]) + ) + ); + } + } + + propertyAccessStack.pop(); + return result; +} + +export function transformBindingVariableDeclaration( + context: TransformationContext, + bindingPattern: ts.BindingPattern, + initializer?: ts.Expression +): lua.Statement[] { + const statements: lua.Statement[] = []; + + // For object, nested or rest bindings fall back to transformBindingPattern + const isComplexBindingElement = (e: ts.ArrayBindingElement) => + ts.isBindingElement(e) && (!ts.isIdentifier(e.name) || e.dotDotDotToken); + + if (ts.isObjectBindingPattern(bindingPattern) || bindingPattern.elements.some(isComplexBindingElement)) { + let table: lua.Expression; + if (initializer) { + // Contain the expression in a temporary variable + let expression = context.transformExpression(initializer); + if (isMultiReturnCall(context, initializer)) { + expression = wrapInTable(expression); + } + const { precedingStatements: moveStatements, result: movedExpr } = transformInPrecedingStatementScope( + context, + () => moveToPrecedingTemp(context, expression, initializer) + ); + statements.push(...moveStatements); + table = movedExpr; + } else { + table = lua.createAnonymousIdentifier(); + } + statements.push(...transformBindingPattern(context, bindingPattern, table)); + return statements; + } + + const vars = + bindingPattern.elements.length > 0 + ? bindingPattern.elements.map(e => transformArrayBindingElement(context, e)) + : lua.createAnonymousIdentifier(); + + if (initializer) { + if (isMultiReturnCall(context, initializer)) { + // Don't unpack LuaMultiReturn functions + statements.push( + ...createLocalOrExportedOrGlobalDeclaration( + context, + vars, + context.transformExpression(initializer), + initializer + ) + ); + } else if (ts.isArrayLiteralExpression(initializer)) { + // Don't unpack array literals + const values = + initializer.elements.length > 0 + ? initializer.elements.map(e => context.transformExpression(e)) + : lua.createNilLiteral(); + statements.push(...createLocalOrExportedOrGlobalDeclaration(context, vars, values, initializer)); + } else { + // use unpack(thing, 1, #bindingItems) to unpack + const unpackedInitializer = createBoundedUnpackCall( + context, + context.transformExpression(initializer), + bindingPattern.elements.length, + initializer + ); + statements.push( + ...createLocalOrExportedOrGlobalDeclaration(context, vars, unpackedInitializer, initializer) + ); + } + } else { + statements.push( + ...createLocalOrExportedOrGlobalDeclaration(context, vars, lua.createNilLiteral(), initializer) + ); + } + + for (const element of bindingPattern.elements) { + if (!ts.isOmittedExpression(element) && element.initializer) { + const variableName = transformIdentifier(context, element.name as ts.Identifier); + const identifier = addExportToIdentifier(context, variableName); + statements.push( + lua.createIfStatement( + lua.createBinaryExpression(identifier, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator), + lua.createBlock([ + lua.createAssignmentStatement(identifier, context.transformExpression(element.initializer)), + ]) + ) + ); + } + } + + return statements; +} + +// TODO: FunctionVisitor +export function transformVariableDeclaration( + context: TransformationContext, + statement: ts.VariableDeclaration +): lua.Statement[] { + if (statement.initializer && statement.type) { + const initializerType = context.checker.getTypeAtLocation(statement.initializer); + const varType = context.checker.getTypeFromTypeNode(statement.type); + validateAssignment(context, statement.initializer, initializerType, varType); + } + + if (ts.isIdentifier(statement.name)) { + // Find variable identifier + const identifierName = transformIdentifier(context, statement.name); + const value = statement.initializer && context.transformExpression(statement.initializer); + + // Wrap functions being assigned to a type that contains additional properties in a callable table + // This catches 'const foo = function() {}; foo.bar = "FOOBAR";' + const wrappedValue = value && shouldWrapInitializerInCallableTable() ? createCallableTable(value) : value; + + return createLocalOrExportedOrGlobalDeclaration(context, identifierName, wrappedValue, statement); + } else if (ts.isArrayBindingPattern(statement.name) || ts.isObjectBindingPattern(statement.name)) { + return transformBindingVariableDeclaration(context, statement.name, statement.initializer); + } else { + return assertNever(statement.name); + } + + function shouldWrapInitializerInCallableTable() { + assert(statement.initializer); + const initializer = ts.skipOuterExpressions(statement.initializer); + // do not wrap if not a function expression + if (!ts.isFunctionExpression(initializer) && !ts.isArrowFunction(initializer)) return false; + // Skip named function expressions because they will have been wrapped already + if (ts.isFunctionExpression(initializer) && initializer.name) return false; + return isFunctionTypeWithProperties(context, context.checker.getTypeAtLocation(statement.name)); + } +} + +export function checkVariableDeclarationList(context: TransformationContext, node: ts.VariableDeclarationList): void { + if ((node.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using | ts.NodeFlags.AwaitUsing)) === 0) { + const token = ts.getOriginalNode(node).getFirstToken(); + assert(token); + context.diagnostics.push(unsupportedVarDeclaration(token)); + } +} + +export const transformVariableStatement: FunctionVisitor = (node, context) => { + checkVariableDeclarationList(context, node.declarationList); + return node.declarationList.declarations.flatMap(declaration => transformVariableDeclaration(context, declaration)); +}; diff --git a/src/transformation/visitors/void.ts b/src/transformation/visitors/void.ts new file mode 100644 index 000000000..c942e859c --- /dev/null +++ b/src/transformation/visitors/void.ts @@ -0,0 +1,15 @@ +import * as ts from "typescript"; +import * as lua from "../../LuaAST"; +import { FunctionVisitor } from "../context"; +import { wrapInStatement } from "./expression-statement"; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void +export const transformVoidExpression: FunctionVisitor = (node, context) => { + // If content is a literal it is safe to replace the entire expression with nil + if (!ts.isLiteralExpression(node.expression)) { + const statements = wrapInStatement(context.transformExpression(node.expression)); + if (statements) context.addPrecedingStatements(statements); + } + + return lua.createNilLiteral(); +}; diff --git a/src/transpilation/bundle.ts b/src/transpilation/bundle.ts new file mode 100644 index 000000000..b030bddfe --- /dev/null +++ b/src/transpilation/bundle.ts @@ -0,0 +1,163 @@ +import * as path from "path"; +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +import { CompilerOptions, LuaTarget } from "../CompilerOptions"; +import { escapeString, tstlHeader } from "../LuaPrinter"; +import { cast, formatPathToLuaPath, isNonNull, trimExtension } from "../utils"; +import { couldNotFindBundleEntryPoint } from "./diagnostics"; +import { getEmitOutDir, getEmitPathRelativeToOutDir, getProjectRoot } from "./transpiler"; +import { EmitFile, ProcessedFile } from "./utils"; + +const createModulePath = (pathToResolve: string, program: ts.Program) => + escapeString(formatPathToLuaPath(trimExtension(getEmitPathRelativeToOutDir(pathToResolve, program)))); + +// Override `require` to read from ____modules table. +function requireOverride(options: CompilerOptions) { + const runModule = + options.luaTarget === LuaTarget.Lua50 + ? "if (table.getn(arg) > 0) then value = module(unpack(arg)) else value = module(file) end" + : 'if (select("#", ...) > 0) then value = module(...) else value = module(file) end'; + return ` +local ____modules = {} +local ____moduleCache = {} +local ____originalRequire = require +local function require(file, ...) + if ____moduleCache[file] then + return ____moduleCache[file].value + end + if ____modules[file] then + local module = ____modules[file] + local value = nil + ${runModule} + ____moduleCache[file] = { value = value } + return value + else + if ____originalRequire then + return ____originalRequire(file) + else + error("module '" .. file .. "' not found") + end + end +end +`; +} + +export const sourceMapTracebackBundlePlaceholder = "{#SourceMapTracebackBundle}"; + +type SourceMapLineData = number | { line: number; file: string }; + +export function printStackTraceBundleOverride(rootNode: SourceNode): string { + const map: Record = {}; + const getLineNumber = (line: number, fallback: number) => { + const data: SourceMapLineData | undefined = map[line]; + if (data === undefined) { + return fallback; + } + if (typeof data === "number") { + return data; + } + return data.line; + }; + const transformLineData = (data: SourceMapLineData) => { + if (typeof data === "number") { + return data; + } + return `{line = ${data.line}, file = "${data.file}"}`; + }; + + let currentLine = 1; + rootNode.walk((chunk, mappedPosition) => { + if (mappedPosition.line !== undefined && mappedPosition.line > 0) { + const line = getLineNumber(currentLine, mappedPosition.line); + + map[currentLine] = { + line, + file: path.basename(mappedPosition.source), + }; + } + + currentLine += chunk.split("\n").length - 1; + }); + + const mapItems = Object.entries(map).map(([line, original]) => `["${line}"] = ${transformLineData(original)}`); + const mapString = "{" + mapItems.join(",") + "}"; + + return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`; +} + +export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [ts.Diagnostic[], EmitFile] { + const diagnostics: ts.Diagnostic[] = []; + + const options = program.getCompilerOptions() as CompilerOptions; + const bundleFile = cast(options.luaBundle, isNonNull); + const entryModule = cast(options.luaBundleEntry, isNonNull); + + // Resolve project settings relative to project file. + const resolvedEntryModule = path.resolve(getProjectRoot(program), entryModule); + const outputPath = path.resolve(getEmitOutDir(program), bundleFile); + const entryModuleFilePath = + program.getSourceFile(entryModule)?.fileName ?? program.getSourceFile(resolvedEntryModule)?.fileName; + + if (entryModuleFilePath === undefined) { + diagnostics.push(couldNotFindBundleEntryPoint(entryModule)); + } + + // For each file: [""] = function() end, + const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(f.fileName, program))); + + // Create ____modules table containing all entries from moduleTableEntries + const moduleTable = createModuleTableNode(moduleTableEntries); + + // return require("") + const args = options.luaTarget === LuaTarget.Lua50 ? "unpack(arg == nil and {} or arg)" : "..."; + // 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) { + // Generates SourceMapTraceback for the entire file + footers.push('local __TS__SourceMapTraceBack = require("lualib_bundle").__TS__SourceMapTraceBack\n'); + footers.push(`${sourceMapTracebackBundlePlaceholder}\n`); + } + + const sourceChunks = [requireOverride(options), moduleTable, ...footers, entryPoint]; + + if (!options.noHeader) { + sourceChunks.unshift(tstlHeader); + } + + const bundleNode = joinSourceChunks(sourceChunks); + let { code, map } = bundleNode.toStringWithSourceMap(); + code = code.replace(sourceMapTracebackBundlePlaceholder, printStackTraceBundleOverride(bundleNode)); + + return [ + diagnostics, + { + outputPath, + code, + sourceMap: map.toString(), + sourceFiles: files.flatMap(x => x.sourceFiles ?? []), + }, + ]; +} + +function moduleSourceNode({ code, sourceMapNode }: ProcessedFile, modulePath: string): SourceNode { + const tableEntryHead = `[${modulePath}] = function(...) \n`; + const tableEntryTail = " end,\n"; + + return joinSourceChunks([tableEntryHead, sourceMapNode ?? code, tableEntryTail]); +} + +function createModuleTableNode(fileChunks: SourceChunk[]): SourceNode { + const tableHead = "____modules = {\n"; + const tableEnd = "}\n"; + + return joinSourceChunks([tableHead, ...fileChunks, tableEnd]); +} + +type SourceChunk = string | SourceNode; + +function joinSourceChunks(chunks: SourceChunk[]): SourceNode { + return new SourceNode(null, null, null, chunks); +} diff --git a/src/transpilation/diagnostics.ts b/src/transpilation/diagnostics.ts new file mode 100644 index 000000000..348542ac5 --- /dev/null +++ b/src/transpilation/diagnostics.ts @@ -0,0 +1,61 @@ +import * as ts from "typescript"; +import { createSerialDiagnosticFactory } from "../utils"; + +const createDiagnosticFactory = ( + getMessage: (...args: TArgs) => string, + category: ts.DiagnosticCategory = ts.DiagnosticCategory.Error +) => createSerialDiagnosticFactory((...args: TArgs) => ({ messageText: getMessage(...args), category })); + +export const couldNotResolveRequire = createDiagnosticFactory( + (requirePath: string, containingFile: string) => + `Could not resolve lua source files for require path '${requirePath}' in file ${containingFile}.` +); + +export const couldNotReadDependency = createDiagnosticFactory( + (dependency: string) => `Could not read content of resolved dependency ${dependency}.` +); + +export const toLoadItShouldBeTranspiled = createDiagnosticFactory( + (kind: string, transform: string) => + `To load "${transform}" ${kind} it should be transpiled or "ts-node" should be installed.` +); + +export const couldNotResolveFrom = createDiagnosticFactory( + (kind: string, transform: string, base: string) => `Could not resolve "${transform}" ${kind} from "${base}".` +); + +export const shouldHaveAExport = createDiagnosticFactory( + (kind: string, transform: string, importName: string) => + `"${transform}" ${kind} should have a "${importName}" export.` +); + +export const transformerShouldBeATsTransformerFactory = createDiagnosticFactory( + (transform: string) => + `"${transform}" transformer should be a ts.TransformerFactory or an object with ts.TransformerFactory values.` +); + +export const couldNotFindBundleEntryPoint = createDiagnosticFactory( + (entryPoint: string) => `Could not find bundle entry point '${entryPoint}'. It should be a file in the project.` +); + +export const luaBundleEntryIsRequired = createDiagnosticFactory( + () => "'luaBundleEntry' is required when 'luaBundle' is enabled." +); + +export const usingLuaBundleWithInlineMightGenerateDuplicateCode = createSerialDiagnosticFactory(() => ({ + category: ts.DiagnosticCategory.Warning, + messageText: + "Using 'luaBundle' with 'luaLibImport: \"inline\"' might generate duplicate code. " + + "It is recommended to use 'luaLibImport: \"require\"'.", +})); + +export const cannotBundleLibrary = createDiagnosticFactory( + () => + 'Cannot bundle projects with "buildmode": "library". Projects including the library can still bundle (which will include external library files).' +); + +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." +); diff --git a/src/transpilation/find-lua-requires.ts b/src/transpilation/find-lua-requires.ts new file mode 100644 index 000000000..90be465e6 --- /dev/null +++ b/src/transpilation/find-lua-requires.ts @@ -0,0 +1,168 @@ +export interface LuaRequire { + from: number; + to: number; + requirePath: string; +} + +export function findLuaRequires(lua: string): LuaRequire[] { + return findRequire(lua, 0); +} + +function findRequire(lua: string, offset: number): LuaRequire[] { + const result = []; + + while (offset < lua.length) { + const c = lua[offset]; + if ( + c === "r" && + (offset === 0 || + isWhitespace(lua[offset - 1]) || + lua[offset - 1] === "]" || + lua[offset - 1] === "(" || + lua[offset - 1] === "[") + ) { + const m = matchRequire(lua, offset); + if (m.matched) { + offset = m.match.to + 1; + result.push(m.match); + } else { + offset = m.end; + } + } else if (c === '"' || c === "'") { + offset = readString(lua, offset, c).offset; // Skip string and surrounding quotes + } else if (c === "-" && offset + 1 < lua.length && lua[offset + 1] === "-") { + offset = skipComment(lua, offset); + } else { + offset++; + } + } + + return result; +} + +type MatchResult = { matched: true; match: T } | { matched: false; end: number }; + +function matchRequire(lua: string, offset: number): MatchResult { + const start = offset; + for (const c of "require") { + if (offset > lua.length) { + return { matched: false, end: offset }; + } + + if (lua[offset] !== c) { + return { matched: false, end: offset }; + } + offset++; + } + + offset = skipWhitespace(lua, offset); + + let hasParentheses = false; + + if (offset > lua.length) { + return { matched: false, end: offset }; + } else { + if (lua[offset] === "(") { + hasParentheses = true; + offset++; + offset = skipWhitespace(lua, offset); + } else if (lua[offset] === '"' || lua[offset] === "'") { + // require without parentheses + } else { + // otherwise fail match + return { matched: false, end: offset }; + } + } + + if (offset > lua.length || (lua[offset] !== '"' && lua[offset] !== "'")) { + return { matched: false, end: offset }; + } + + const { value: requireString, offset: offsetAfterString } = readString(lua, offset, lua[offset]); + offset = offsetAfterString; // Skip string and surrounding quotes + + if (hasParentheses) { + offset = skipWhitespace(lua, offset); + + if (offset > lua.length || lua[offset] !== ")") { + return { matched: false, end: offset }; + } + + offset++; + } + + return { matched: true, match: { from: start, to: offset - 1, requirePath: requireString } }; +} + +function readString(lua: string, offset: number, delimiter: string): { value: string; offset: number } { + expect(lua, offset, delimiter); + offset++; + + let start = offset; + let result = ""; + + let escaped = false; + while (offset < lua.length && (lua[offset] !== delimiter || escaped)) { + if (lua[offset] === "\\" && !escaped) { + escaped = true; + } else { + if (lua[offset] === delimiter) { + result += lua.slice(start, offset - 1); + start = offset; + } + escaped = false; + } + + offset++; + } + + if (offset < lua.length) { + expect(lua, offset, delimiter); + } + + result += lua.slice(start, offset); + return { value: result, offset: offset + 1 }; +} + +function skipWhitespace(lua: string, offset: number): number { + while (offset < lua.length && isWhitespace(lua[offset])) { + offset++; + } + return offset; +} + +function isWhitespace(c: string): boolean { + return c === " " || c === "\t" || c === "\r" || c === "\n"; +} + +function skipComment(lua: string, offset: number): number { + expect(lua, offset, "-"); + expect(lua, offset + 1, "-"); + offset += 2; + + if (offset + 1 < lua.length && lua[offset] === "[" && lua[offset + 1] === "[") { + return skipMultiLineComment(lua, offset); + } else { + return skipSingleLineComment(lua, offset); + } +} + +function skipMultiLineComment(lua: string, offset: number): number { + while (offset < lua.length && !(lua[offset] === "]" && lua[offset - 1] === "]")) { + offset++; + } + return offset + 1; +} + +function skipSingleLineComment(lua: string, offset: number): number { + while (offset < lua.length && lua[offset] !== "\n") { + offset++; + } + return offset + 1; +} + +function expect(lua: string, offset: number, char: string) { + if (lua[offset] !== char) { + throw new Error(`Expected ${char} at position ${offset} but found ${lua[offset]}`); + } +} diff --git a/src/transpilation/index.ts b/src/transpilation/index.ts new file mode 100644 index 000000000..d4e890719 --- /dev/null +++ b/src/transpilation/index.ts @@ -0,0 +1,118 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as ts from "typescript"; +import { parseConfigFileWithSystem } from "../cli/tsconfig"; +import { CompilerOptions } from "../CompilerOptions"; +import { normalizeSlashes } from "../utils"; +import { createEmitOutputCollector, TranspiledFile } from "./output-collector"; +import { EmitResult, Transpiler } from "./transpiler"; + +export { Plugin } from "./plugins"; +export * from "./transpile"; +export * from "./transpiler"; +export { EmitHost } from "./utils"; +export { TranspiledFile }; + +export function transpileFiles( + rootNames: string[], + options: CompilerOptions = {}, + writeFile?: ts.WriteFileCallback +): EmitResult { + const program = ts.createProgram(rootNames, options); + const preEmitDiagnostics = ts.getPreEmitDiagnostics(program); + const { diagnostics: transpileDiagnostics, emitSkipped } = new Transpiler().emit({ program, writeFile }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics]); + + return { diagnostics: [...diagnostics], emitSkipped }; +} + +export function transpileProject( + configFileName: string, + optionsToExtend?: CompilerOptions, + writeFile?: ts.WriteFileCallback +): EmitResult { + const parseResult = parseConfigFileWithSystem(configFileName, optionsToExtend); + if (parseResult.errors.length > 0) { + return { diagnostics: parseResult.errors, emitSkipped: true }; + } + + return transpileFiles(parseResult.fileNames, parseResult.options, writeFile); +} + +const libCache: { [key: string]: ts.SourceFile } = {}; + +/** @internal */ +export function createVirtualProgram(input: Record, options: CompilerOptions = {}): ts.Program { + const normalizedFiles: Record = {}; + for (const [path, file] of Object.entries(input)) { + normalizedFiles[normalizeSlashes(path)] = file; + } + const compilerHost: ts.CompilerHost = { + fileExists: fileName => fileName in normalizedFiles || ts.sys.fileExists(fileName), + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => "", + getDefaultLibFileName: ts.getDefaultLibFileName, + readFile: () => "", + getNewLine: () => "\n", + useCaseSensitiveFileNames: () => false, + writeFile() {}, + + getSourceFile(fileName) { + if (fileName in normalizedFiles) { + return ts.createSourceFile(fileName, normalizedFiles[fileName], ts.ScriptTarget.Latest, false); + } + + let filePath: string | undefined; + + if (fileName.startsWith("lib.")) { + const typeScriptDir = path.dirname(require.resolve("typescript")); + filePath = path.join(typeScriptDir, fileName); + } + + if (fileName.includes("language-extensions")) { + const dtsName = fileName.replace(/(\.d)?(\.ts)$/, ".d.ts"); + filePath = path.resolve(dtsName); + } + + if (filePath !== undefined) { + if (libCache[fileName]) return libCache[fileName]; + const content = fs.readFileSync(filePath, "utf8"); + libCache[fileName] = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, false); + return libCache[fileName]; + } + }, + }; + + return ts.createProgram(Object.keys(normalizedFiles), options, compilerHost); +} + +export interface TranspileVirtualProjectResult { + diagnostics: ts.Diagnostic[]; + transpiledFiles: TranspiledFile[]; +} + +export function transpileVirtualProject( + files: Record, + options: CompilerOptions = {} +): TranspileVirtualProjectResult { + const program = createVirtualProgram(files, options); + const preEmitDiagnostics = ts.getPreEmitDiagnostics(program); + const collector = createEmitOutputCollector(); + const { diagnostics: transpileDiagnostics } = new Transpiler().emit({ program, writeFile: collector.writeFile }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics]); + + return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; +} + +export interface TranspileStringResult { + diagnostics: ts.Diagnostic[]; + file?: TranspiledFile; +} + +export function transpileString(main: string, options: CompilerOptions = {}): TranspileStringResult { + const { diagnostics, transpiledFiles } = transpileVirtualProject({ "main.ts": main }, options); + return { + diagnostics, + file: transpiledFiles.find(({ sourceFiles }) => sourceFiles.some(f => f.fileName === "main.ts")), + }; +} diff --git a/src/transpilation/output-collector.ts b/src/transpilation/output-collector.ts new file mode 100644 index 000000000..1458b2403 --- /dev/null +++ b/src/transpilation/output-collector.ts @@ -0,0 +1,44 @@ +import * as ts from "typescript"; +import { intersection, union } from "../utils"; + +export interface TranspiledFile { + outPath: string; + sourceFiles: ts.SourceFile[]; + lua?: string; + luaSourceMap?: string; + declaration?: string; + declarationMap?: string; + /** @internal */ + js?: string; + /** @internal */ + jsSourceMap?: string; +} + +export function createEmitOutputCollector(luaExtension = ".lua") { + const files: TranspiledFile[] = []; + const writeFile: ts.WriteFileCallback = (fileName, data, _bom, _onError, sourceFiles = []) => { + let file = files.find(f => intersection(f.sourceFiles, sourceFiles).length > 0); + if (!file) { + file = { outPath: fileName, sourceFiles: [...sourceFiles] }; + files.push(file); + } else { + file.sourceFiles = union(file.sourceFiles, sourceFiles); + } + + if (fileName.endsWith(luaExtension)) { + file.lua = data; + } else if (fileName.endsWith(`${luaExtension}.map`)) { + file.luaSourceMap = data; + } else if (fileName.endsWith(".js")) { + file.js = data; + } else if (fileName.endsWith(".js.map")) { + file.jsSourceMap = data; + } else if (fileName.endsWith(".d.ts")) { + file.declaration = data; + } else if (fileName.endsWith(".d.ts.map")) { + file.declarationMap = data; + } + }; + + return { writeFile, files }; +} diff --git a/src/transpilation/plugins.ts b/src/transpilation/plugins.ts new file mode 100644 index 000000000..f84af2bc9 --- /dev/null +++ b/src/transpilation/plugins.ts @@ -0,0 +1,110 @@ +import * as ts from "typescript"; +import { EmitHost } from ".."; +import { CompilerOptions } from "../CompilerOptions"; +import { Printer } from "../LuaPrinter"; +import { Visitors } from "../transformation/context"; +import { EmitFile, getConfigDirectory, ProcessedFile, resolvePlugin } from "./utils"; +import * as performance from "../measure-performance"; + +export interface Plugin { + /** + * An augmentation to the map of visitors that transform TypeScript AST to Lua AST. + * + * Key is a `SyntaxKind` of a processed node. + */ + visitors?: Visitors; + + /** + * A function that converts Lua AST to a string. + * + * At most one custom printer can be provided across all plugins. + */ + printer?: Printer; + + /** + * This function is called before transpilation of the TypeScript program starts. + */ + beforeTransform?: (program: ts.Program, options: CompilerOptions, emitHost: EmitHost) => ts.Diagnostic[] | void; + + /** + * This function is called after translating the input program to Lua, but before resolving dependencies or bundling. + */ + afterPrint?: ( + program: ts.Program, + options: CompilerOptions, + emitHost: EmitHost, + result: ProcessedFile[] + ) => ts.Diagnostic[] | void; + + /** + * This function is called after translating the input program to Lua, after resolving dependencies and after bundling. + */ + beforeEmit?: ( + program: ts.Program, + options: CompilerOptions, + emitHost: EmitHost, + result: EmitFile[] + ) => ts.Diagnostic[] | void; + + /** + * This function is called after translating the input program to Lua, after resolving dependencies, after bundling and writing files to disk. + */ + afterEmit?: ( + program: ts.Program, + options: CompilerOptions, + emitHost: EmitHost, + result: EmitFile[] + ) => ts.Diagnostic[] | void; + + /** + * This function is called when trying to resolve the .lua file corresponding to a Lua require statement. Allows you to provide + * your own module resolution logic. If return value is undefined, regular module resolution is done. + */ + moduleResolution?: ( + moduleIdentifier: string, + requiringFile: string, + options: CompilerOptions, + emitHost: EmitHost + ) => string | undefined; +} + +export function getPlugins(program: ts.Program): { diagnostics: ts.Diagnostic[]; plugins: Plugin[] } { + performance.startSection("getPlugins"); + const diagnostics: ts.Diagnostic[] = []; + const pluginsFromOptions: Plugin[] = []; + const options = program.getCompilerOptions() as CompilerOptions; + + for (const [index, pluginOption] of (options.luaPlugins ?? []).entries()) { + const optionName = `tstl.luaPlugins[${index}]`; + + 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; + pluginsFromOptions.push(plugin); + } + + if (options.tstlVerbose) { + console.log(`Loaded ${pluginsFromOptions.length} plugins`); + } + + performance.endSection("getPlugins"); + + return { diagnostics, plugins: pluginsFromOptions }; +} diff --git a/src/transpilation/resolve.ts b/src/transpilation/resolve.ts new file mode 100644 index 000000000..bd16773e9 --- /dev/null +++ b/src/transpilation/resolve.ts @@ -0,0 +1,481 @@ +import * as path from "path"; +import * as resolve from "enhanced-resolve"; +import * as ts from "typescript"; +import * as fs from "fs"; +import { EmitHost, ProcessedFile } from "./utils"; +import { SourceNode } from "source-map"; +import { getEmitPathRelativeToOutDir, getProjectRoot, getSourceDir } from "./transpiler"; +import { formatPathToLuaPath, normalizeSlashes, trimExtension } from "../utils"; +import { couldNotReadDependency, couldNotResolveRequire } from "./diagnostics"; +import { BuildMode, CompilerOptions } from "../CompilerOptions"; +import { findLuaRequires, LuaRequire } from "./find-lua-requires"; +import { Plugin } from "./plugins"; +import * as picomatch from "picomatch"; + +const resolver = resolve.ResolverFactory.createResolver({ + extensions: [".lua"], + enforceExtension: true, // Resolved file must be a lua file + fileSystem: { ...new resolve.CachedInputFileSystem(fs, 0) }, + useSyncFileSystemCalls: true, + conditionNames: ["require", "node", "tstl", "default"], + symlinks: false, // Do not resolve symlinks to their original paths (that breaks node_modules detection) +}); + +interface ResolutionResult { + resolvedFiles: ProcessedFile[]; + diagnostics: ts.Diagnostic[]; +} + +class ResolutionContext { + private noResolvePaths: picomatch.Matcher[]; + + public diagnostics: ts.Diagnostic[] = []; + public resolvedFiles = new Map(); + + constructor( + public readonly program: ts.Program, + public readonly options: CompilerOptions, + private readonly emitHost: EmitHost, + private readonly plugins: Plugin[] + ) { + const unique = [...new Set(options.noResolvePaths)]; + const matchers = unique.map(x => picomatch(x)); + this.noResolvePaths = matchers; + } + + public addAndResolveDependencies(file: ProcessedFile): void { + if (this.resolvedFiles.has(file.fileName)) return; + this.resolvedFiles.set(file.fileName, file); + + // Do this backwards so the replacements do not mess with the positions of the previous requires + for (const required of findLuaRequires(file.code).reverse()) { + // Do not resolve noResolution paths + if (required.requirePath.startsWith("@NoResolution:")) { + // Remove @NoResolution prefix if not building in library mode + if (!isBuildModeLibrary(this.program)) { + const path = required.requirePath.replace("@NoResolution:", ""); + replaceRequireInCode(file, required, path, this.options.extension); + replaceRequireInSourceMap(file, required, path, this.options.extension); + } + + // Skip + continue; + } + // Try to resolve the import starting from the directory `file` is in + this.resolveImport(file, required); + } + } + + public resolveImport(file: ProcessedFile, required: LuaRequire): void { + // Do no resolve lualib - always use the lualib of the application entry point, not the lualib from external packages + if (required.requirePath === "lualib_bundle") { + this.resolvedFiles.set("lualib_bundle", { fileName: "lualib_bundle", code: "" }); + return; + } + + if (this.noResolvePaths.find(isMatch => isMatch(required.requirePath))) { + if (this.options.tstlVerbose) { + console.log( + `Skipping module resolution of ${required.requirePath} as it is in the tsconfig noResolvePaths.` + ); + } + return; + } + + const dependencyPath = + this.resolveDependencyPathsWithPlugins(file, required.requirePath) ?? + this.resolveDependencyPath(file, required.requirePath); + + if (!dependencyPath) return this.couldNotResolveImport(required, file); + + if (this.options.tstlVerbose) { + console.log(`Resolved ${required.requirePath} to ${normalizeSlashes(dependencyPath)}`); + } + + this.processDependency(dependencyPath); + // Figure out resolved require path and dependency output path + if (shouldRewriteRequires(dependencyPath, this.program)) { + const resolvedRequire = getEmitPathRelativeToOutDir(dependencyPath, this.program); + replaceRequireInCode(file, required, resolvedRequire, this.options.extension); + replaceRequireInSourceMap(file, required, resolvedRequire, this.options.extension); + } + } + + private resolveDependencyPathsWithPlugins(requiringFile: ProcessedFile, dependency: string) { + const requiredFromLuaFile = requiringFile.fileName.endsWith(".lua"); + for (const plugin of this.plugins) { + if (plugin.moduleResolution != null) { + const pluginResolvedPath = plugin.moduleResolution( + dependency, + requiringFile.fileName, + this.options, + this.emitHost + ); + if (pluginResolvedPath !== undefined) { + // If resolved path is absolute no need to further resolve it + if (path.isAbsolute(pluginResolvedPath)) { + return pluginResolvedPath; + } + + // If lua file is in node_module + if (requiredFromLuaFile && isNodeModulesFile(requiringFile.fileName)) { + // If requiring file is in lua module, try to resolve sibling in that file first + const resolvedNodeModulesFile = this.resolveLuaDependencyPathFromNodeModules( + requiringFile, + pluginResolvedPath + ); + if (resolvedNodeModulesFile) { + if (this.options.tstlVerbose) { + console.log( + `Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.` + ); + } + return resolvedNodeModulesFile; + } + } + + const resolvedPath = this.formatPathToFile(pluginResolvedPath, requiringFile); + const fileFromPath = this.getFileFromPath(resolvedPath); + + if (fileFromPath) { + if (this.options.tstlVerbose) { + console.log( + `Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.` + ); + } + return fileFromPath; + } + } + } + } + } + + public processedDependencies = new Set(); + + private formatPathToFile(targetPath: string, required: ProcessedFile) { + const isRelative = ["/", "./", "../"].some(p => targetPath.startsWith(p)); + + // // If the import is relative, always resolve it relative to the requiring file + // // If the import is not relative, resolve it relative to options.baseUrl if it is set + const fileDirectory = path.dirname(required.fileName); + const relativeTo = isRelative ? fileDirectory : this.options.baseUrl ?? fileDirectory; + + // // Check if file is a file in the project + const resolvedPath = path.join(relativeTo, targetPath); + return resolvedPath; + } + + private processDependency(dependencyPath: string): void { + if (this.processedDependencies.has(dependencyPath)) return; + this.processedDependencies.add(dependencyPath); + + if (!shouldIncludeDependency(dependencyPath, this.program)) return; + + // If dependency is not part of project, add dependency to output and resolve its dependencies recursively + const dependencyContent = this.emitHost.readFile(dependencyPath); + if (dependencyContent === undefined) { + this.diagnostics.push(couldNotReadDependency(dependencyPath)); + return; + } + + const dependency = { + fileName: dependencyPath, + code: dependencyContent, + }; + this.addAndResolveDependencies(dependency); + } + + private couldNotResolveImport(required: LuaRequire, file: ProcessedFile): void { + const fallbackRequire = fallbackResolve(required, getSourceDir(this.program), path.dirname(file.fileName)); + replaceRequireInCode(file, required, fallbackRequire, this.options.extension); + replaceRequireInSourceMap(file, required, fallbackRequire, this.options.extension); + + this.diagnostics.push( + couldNotResolveRequire(required.requirePath, path.relative(getProjectRoot(this.program), file.fileName)) + ); + } + + private resolveDependencyPath(requiringFile: ProcessedFile, dependency: string): string | undefined { + const fileDirectory = path.dirname(requiringFile.fileName); + if (this.options.tstlVerbose) { + console.log(`Resolving "${dependency}" from ${normalizeSlashes(requiringFile.fileName)}`); + } + + const requiredFromLuaFile = requiringFile.fileName.endsWith(".lua"); + const dependencyPath = requiredFromLuaFile ? luaRequireToPath(dependency) : dependency; + + if (requiredFromLuaFile && isNodeModulesFile(requiringFile.fileName)) { + // If requiring file is in lua module, try to resolve sibling in that file first + const resolvedNodeModulesFile = this.resolveLuaDependencyPathFromNodeModules(requiringFile, dependencyPath); + if (resolvedNodeModulesFile) return resolvedNodeModulesFile; + } + + // 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); + if (resolveResult) return resolveResult; + } catch (e: any) { + // resolveSync errors if it fails to resolve + if (this.options.tstlVerbose && e.details) { + // Output resolver log + console.log(e.details); + } + } + + return undefined; + } + + private resolveLuaDependencyPathFromNodeModules( + requiringFile: ProcessedFile, + dependency: string + ): string | undefined { + // We don't know for sure where the lua root is, so guess it is at package root + const splitPath = path.normalize(requiringFile.fileName).split(path.sep); + let packageRootIndex = splitPath.lastIndexOf("node_modules") + 2; + let packageRoot = splitPath.slice(0, packageRootIndex).join(path.sep); + + while (packageRootIndex < splitPath.length) { + // Try to find lua file relative to currently guessed Lua root + const resolvedPath = path.join(packageRoot, dependency); + const fileFromPath = this.getFileFromPath(resolvedPath); + if (fileFromPath) { + return fileFromPath; + } else { + // Did not find file at current root, try again one directory deeper + packageRoot = path.join(packageRoot, splitPath[packageRootIndex++]); + } + } + + return undefined; + } + + // value is false if already searched but not found + private pathToFile = new Map(); + + private getFileFromPath(resolvedPath: string): string | undefined { + const existingFile = this.pathToFile.get(resolvedPath); + if (existingFile) return existingFile; + if (existingFile === false) return undefined; + + const file = this.searchForFileFromPath(resolvedPath); + this.pathToFile.set(resolvedPath, file ?? false); + return file; + } + + private searchForFileFromPath(resolvedPath: string): string | undefined { + const possibleProjectFiles = [ + resolvedPath, // JSON files need their extension as part of the import path, caught by this branch, + resolvedPath + ".ts", // Regular ts file + path.join(resolvedPath, "index.ts"), // Index ts file, + resolvedPath + ".tsx", // tsx file + path.join(resolvedPath, "index.tsx"), // tsx index + ]; + + for (const possibleFile of possibleProjectFiles) { + if (isProjectFile(possibleFile, this.program)) { + return possibleFile; + } + } + + // Check if this is a lua file in the project sources + const possibleLuaProjectFiles = [ + resolvedPath + ".lua", // lua file in sources + path.join(resolvedPath, "index.lua"), // lua index file in sources + path.join(resolvedPath, "init.lua"), // lua looks for /init.lua if it cannot find .lua + ]; + + for (const possibleFile of possibleLuaProjectFiles) { + if (this.emitHost.fileExists(possibleFile)) { + return possibleFile; + } + } + } + + // Taken from TS and modified: https://github.com/microsoft/TypeScript/blob/88a1e3a1dd8d2d86e844ff1c16d5f041cebcfdb9/src/compiler/moduleSpecifiers.ts#L562 + private tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: ts.MapLike, baseUrl: string) { + const relativeImport = removeTrailingDirectorySeparator(normalizeSlashes(relativeToBaseUrl)); + for (const [importPattern, targetPatterns] of Object.entries(paths)) { + const pattern = removeFileExtension(normalizeSlashes(importPattern)); + const indexOfStar = pattern.indexOf("*"); + if (indexOfStar !== -1) { + // Try to match * to relativeImport + const prefix = pattern.substring(0, indexOfStar); + const suffix = pattern.substring(indexOfStar + 1); + if ( + (relativeImport.length >= prefix.length + suffix.length && + relativeImport.startsWith(prefix) && + relativeImport.endsWith(suffix)) || + (!suffix && relativeImport === removeTrailingDirectorySeparator(prefix)) + ) { + // If import matches *, extract the matched * path + const matchedStar = relativeImport.substring(prefix.length, relativeImport.length - suffix.length); + // Try to resolve to the target patterns with filled in * pattern + for (const target of targetPatterns) { + const file = this.getFileFromPath(path.join(baseUrl, target.replace("*", matchedStar))); + if (file) return file; + } + } + } else if (pattern === relativeImport) { + // If there is no * pattern, check for exact matches and try those targets + for (const target of targetPatterns) { + const file = this.getFileFromPath(path.join(baseUrl, target)); + if (file) return file; + } + } + } + } +} + +export function resolveDependencies( + program: ts.Program, + files: ProcessedFile[], + emitHost: EmitHost, + plugins: Plugin[] +): ResolutionResult { + const options = program.getCompilerOptions() as CompilerOptions; + + const resolutionContext = new ResolutionContext(program, options, emitHost, plugins); + + // Resolve dependencies for all processed files + for (const file of files) { + if (options.tstlVerbose) { + console.log(`Resolving dependencies for ${normalizeSlashes(file.fileName)}`); + } + resolutionContext.addAndResolveDependencies(file); + } + + return { resolvedFiles: [...resolutionContext.resolvedFiles.values()], diagnostics: resolutionContext.diagnostics }; +} + +function shouldRewriteRequires(resolvedDependency: string, program: ts.Program) { + return !isBuildModeLibrary(program) || !isNodeModulesFile(resolvedDependency); +} + +function shouldIncludeDependency(resolvedDependency: string, program: ts.Program) { + // Never include lua files (again) that are transpiled from project sources + if (hasSourceFileInProject(resolvedDependency, program)) return false; + // Always include lua files not in node_modules (internal lua sources) + if (!isNodeModulesFile(resolvedDependency)) return true; + // Only include node_modules files if not in library mode + return !isBuildModeLibrary(program); +} + +function isBuildModeLibrary(program: ts.Program) { + return program.getCompilerOptions().buildMode === BuildMode.Library; +} + +function replaceRequireInCode( + file: ProcessedFile, + originalRequire: LuaRequire, + newRequire: string, + extension: string | undefined +): void { + const requirePath = requirePathForFile(newRequire, extension); + file.code = file.code = + file.code.substring(0, originalRequire.from) + + `require("${requirePath}")` + + file.code.substring(originalRequire.to + 1); +} + +function replaceRequireInSourceMap( + file: ProcessedFile, + originalRequire: LuaRequire, + newRequire: string, + extension?: string | undefined +): void { + const requirePath = requirePathForFile(newRequire, extension); + if (file.sourceMapNode) { + replaceInSourceMap( + file.sourceMapNode, + file.sourceMapNode, + `"${originalRequire.requirePath}"`, + `"${requirePath}"` + ); + } +} + +function requirePathForFile(filePath: string, extension = ".lua"): string { + if (!extension.startsWith(".")) { + extension = `.${extension}`; + } + if (filePath.endsWith(extension)) { + return formatPathToLuaPath(filePath.substring(0, filePath.length - extension.length)); + } else { + return formatPathToLuaPath(filePath); + } +} + +function replaceInSourceMap(node: SourceNode, parent: SourceNode, require: string, resolvedRequire: string): boolean { + if ((!node.children || node.children.length === 0) && node.toString() === require) { + parent.children = [new SourceNode(node.line, node.column, node.source, [resolvedRequire])]; + return true; // Stop after finding the first occurrence + } + + if (node.children) { + for (const c of node.children) { + if (replaceInSourceMap(c, node, require, resolvedRequire)) { + return true; // Occurrence found in one of the children + } + } + } + + return false; // Did not find the require +} + +function isNodeModulesFile(filePath: string): boolean { + return path + .normalize(filePath) + .split(path.sep) + .some(p => p === "node_modules"); +} + +function isProjectFile(file: string, program: ts.Program): boolean { + return program.getSourceFile(file) !== undefined; +} + +function hasSourceFileInProject(filePath: string, program: ts.Program) { + const pathWithoutExtension = trimExtension(filePath); + return ( + isProjectFile(pathWithoutExtension + ".ts", program) || + isProjectFile(pathWithoutExtension + ".tsx", program) || + isProjectFile(pathWithoutExtension + ".json", program) + ); +} + +// Transform an import path to a lua require that is probably not correct, but can be used as fallback when regular resolution fails +function fallbackResolve(required: LuaRequire, sourceRootDir: string, fileDir: string): string { + return formatPathToLuaPath( + path + .normalize(path.join(path.relative(sourceRootDir, fileDir), required.requirePath)) + .split(path.sep) + .filter(s => s !== "." && s !== "..") + .join(path.sep) + ); +} + +function luaRequireToPath(requirePath: string): string { + return requirePath.replace(/\./g, path.sep); +} + +function removeFileExtension(path: string) { + return path.includes(".") ? trimExtension(path) : path; +} + +function removeTrailingDirectorySeparator(path: string) { + return path.endsWith("/") || path.endsWith("\\") ? path.substring(0, -1) : path; +} diff --git a/src/transpilation/transformers.ts b/src/transpilation/transformers.ts new file mode 100644 index 000000000..0080b6e4e --- /dev/null +++ b/src/transpilation/transformers.ts @@ -0,0 +1,221 @@ +import * as ts from "typescript"; +// TODO: Don't depend on CLI? +import * as cliDiagnostics from "../cli/diagnostics"; +import { CompilerOptions, TransformerImport } from "../CompilerOptions"; +import * as diagnosticFactories from "./diagnostics"; +import { getConfigDirectory, resolvePlugin } from "./utils"; + +export function getTransformers( + program: ts.Program, + diagnostics: ts.Diagnostic[], + customTransformers: ts.CustomTransformers, + onSourceFile: (sourceFile: ts.SourceFile) => void +): ts.CustomTransformers { + const luaTransformer: ts.TransformerFactory = () => sourceFile => { + onSourceFile(sourceFile); + return ts.createSourceFile(sourceFile.fileName, "", ts.ScriptTarget.ESNext); + }; + + const transformersFromOptions = loadTransformersFromOptions(program, diagnostics); + + const afterDeclarations = [ + ...(transformersFromOptions.afterDeclarations ?? []), + ...(customTransformers.afterDeclarations ?? []), + ]; + + const options = program.getCompilerOptions() as CompilerOptions; + if (options.noImplicitSelf) { + afterDeclarations.unshift(noImplicitSelfTransformer); + } + + return { + afterDeclarations, + before: [ + ...(customTransformers.before ?? []), + ...(transformersFromOptions.before ?? []), + + ...(transformersFromOptions.after ?? []), + ...(customTransformers.after ?? []), + stripParenthesisExpressionsTransformer, + luaTransformer, + ], + }; +} + +export const noImplicitSelfTransformer: ts.TransformerFactory = () => node => { + const transformSourceFile: ts.Transformer = node => { + const empty = ts.factory.createNotEmittedStatement(undefined!); + ts.addSyntheticLeadingComment(empty, ts.SyntaxKind.MultiLineCommentTrivia, "* @noSelfInFile ", true); + return ts.factory.updateSourceFile(node, [empty, ...node.statements], node.isDeclarationFile); + }; + + return ts.isBundle(node) + ? ts.factory.updateBundle(node, node.sourceFiles.map(transformSourceFile)) + : transformSourceFile(node); +}; + +export const stripParenthesisExpressionsTransformer: ts.TransformerFactory = context => sourceFile => { + // Remove parenthesis expressions before transforming to Lua, so transpiler is not hindered by extra ParenthesizedExpression nodes + function unwrapParentheses(node: ts.Expression) { + while (ts.isParenthesizedExpression(node) && !ts.isOptionalChain(node.expression)) { + node = node.expression; + } + return node; + } + function visit(node: ts.Node): ts.Node { + // For now only call expressions strip their expressions of parentheses, there could be more cases where this is required + if (ts.isCallExpression(node)) { + return ts.factory.updateCallExpression( + node, + unwrapParentheses(node.expression), + node.typeArguments, + node.arguments + ); + } else if (ts.isVoidExpression(node)) { + return ts.factory.updateVoidExpression(node, unwrapParentheses(node.expression)); + } else if (ts.isDeleteExpression(node)) { + return ts.factory.updateDeleteExpression(node, unwrapParentheses(node.expression)); + } + + return ts.visitEachChild(node, visit, context); + } + return ts.visitEachChild(sourceFile, visit, context); +}; + +function loadTransformersFromOptions(program: ts.Program, diagnostics: ts.Diagnostic[]): ts.CustomTransformers { + const customTransformers: Required = { + before: [], + after: [], + afterDeclarations: [], + }; + + const options = program.getCompilerOptions() as CompilerOptions; + if (options.plugins) { + for (const [index, transformerImport] of options.plugins.entries()) { + if (!("transform" in transformerImport)) continue; + const optionName = `compilerOptions.plugins[${index}]`; + + const { error: resolveError, result: factory } = resolvePlugin( + "transformer", + `${optionName}.transform`, + getConfigDirectory(options), + transformerImport.transform, + transformerImport.import + ); + + if (resolveError) diagnostics.push(resolveError); + if (factory === undefined) continue; + + const { error: loadError, transformer } = loadTransformer(optionName, program, factory, transformerImport); + if (loadError) diagnostics.push(loadError); + if (transformer === undefined) continue; + + if (transformer.before) { + customTransformers.before.push(transformer.before); + } + + if (transformer.after) { + customTransformers.after.push(transformer.after); + } + + if (transformer.afterDeclarations) { + customTransformers.afterDeclarations.push(transformer.afterDeclarations); + } + } + } + if (options.jsx === ts.JsxEmit.React) { + customTransformers.before.push(context => { + // if target < ES2017, typescript generates some unnecessary additional transformations in transformJSX. + // We can't control the target compiler option, so we override here. + const patchedContext: ts.TransformationContext = { + ...context, + getCompilerOptions: () => ({ + ...context.getCompilerOptions(), + target: ts.ScriptTarget.ESNext, + }), + }; + return ts.transformJsx(patchedContext); + }); + } + return customTransformers; +} + +type ProgramTransformerFactory = (program: ts.Program, options: Record) => Transformer; +type ConfigTransformerFactory = (options: Record) => Transformer; +type CompilerOptionsTransformerFactory = ( + compilerOptions: CompilerOptions, + options: Record +) => Transformer; +type TypeCheckerTransformerFactory = (typeChecker: ts.TypeChecker, options: Record) => Transformer; +type RawTransformerFactory = Transformer; + +type Transformer = GroupTransformer | ts.TransformerFactory; +interface GroupTransformer { + before?: ts.TransformerFactory; + after?: ts.TransformerFactory; + afterDeclarations?: ts.TransformerFactory; +} + +function loadTransformer( + optionPath: string, + program: ts.Program, + factory: unknown, + { transform, after = false, afterDeclarations = false, type = "program", ...extraOptions }: TransformerImport +): { error?: ts.Diagnostic; transformer?: GroupTransformer } { + let transformer: Transformer; + switch (type) { + case "program": + transformer = (factory as ProgramTransformerFactory)(program, extraOptions); + break; + case "config": + transformer = (factory as ConfigTransformerFactory)(extraOptions); + break; + case "checker": + transformer = (factory as TypeCheckerTransformerFactory)(program.getTypeChecker(), extraOptions); + break; + case "raw": + transformer = factory as RawTransformerFactory; + break; + case "compilerOptions": + transformer = (factory as CompilerOptionsTransformerFactory)(program.getCompilerOptions(), extraOptions); + break; + default: { + const optionName = `--${optionPath}.type`; + return { error: cliDiagnostics.argumentForOptionMustBe(optionName, "program") }; + } + } + + if (typeof after !== "boolean") { + const optionName = `${optionPath}.after`; + return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") }; + } + + if (typeof afterDeclarations !== "boolean") { + const optionName = `${optionPath}.afterDeclarations`; + return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") }; + } + + if (typeof transformer === "function") { + let wrappedTransformer: GroupTransformer; + + if (after) { + wrappedTransformer = { after: transformer }; + } else if (afterDeclarations) { + wrappedTransformer = { afterDeclarations: transformer as ts.TransformerFactory }; + } else { + wrappedTransformer = { before: transformer }; + } + + return { transformer: wrappedTransformer }; + } else { + const isValidGroupTransformer = + typeof transformer === "object" && + (transformer.before ?? transformer.after ?? transformer.afterDeclarations) !== undefined; + + if (!isValidGroupTransformer) { + return { error: diagnosticFactories.transformerShouldBeATsTransformerFactory(transform) }; + } + } + + return { transformer }; +} diff --git a/src/transpilation/transpile.ts b/src/transpilation/transpile.ts new file mode 100644 index 000000000..6adfa5fc4 --- /dev/null +++ b/src/transpilation/transpile.ts @@ -0,0 +1,158 @@ +import * as path from "path"; +import * as ts from "typescript"; +import { CompilerOptions, validateOptions } from "../CompilerOptions"; +import { createPrinter } from "../LuaPrinter"; +import { createVisitorMap, transformSourceFile } from "../transformation"; +import { isNonNull } from "../utils"; +import { Plugin } from "./plugins"; +import { getTransformers } from "./transformers"; +import { EmitHost, ProcessedFile } from "./utils"; +import * as performance from "../measure-performance"; + +export interface TranspileOptions { + program: ts.Program; + sourceFiles?: ts.SourceFile[]; + customTransformers?: ts.CustomTransformers; + plugins?: Plugin[]; +} + +export interface TranspileResult { + diagnostics: ts.Diagnostic[]; + transpiledFiles: ProcessedFile[]; +} + +export function getProgramTranspileResult( + emitHost: EmitHost, + writeFileResult: ts.WriteFileCallback, + { program, sourceFiles: targetSourceFiles, customTransformers = {}, plugins = [] }: TranspileOptions +): TranspileResult { + performance.startSection("beforeTransform"); + + const options = program.getCompilerOptions() as CompilerOptions; + + if (options.tstlVerbose) { + console.log("Parsing project settings"); + } + + const diagnostics = validateOptions(options); + let transpiledFiles: ProcessedFile[] = []; + + if (options.noEmitOnError) { + const preEmitDiagnostics = [ + ...diagnostics, + ...program.getOptionsDiagnostics(), + ...program.getGlobalDiagnostics(), + ]; + + if (targetSourceFiles) { + for (const sourceFile of targetSourceFiles) { + preEmitDiagnostics.push(...program.getSyntacticDiagnostics(sourceFile)); + preEmitDiagnostics.push(...program.getSemanticDiagnostics(sourceFile)); + } + } else { + preEmitDiagnostics.push(...program.getSyntacticDiagnostics()); + preEmitDiagnostics.push(...program.getSemanticDiagnostics()); + } + + if (preEmitDiagnostics.length === 0 && (options.declaration || options.composite)) { + preEmitDiagnostics.push(...program.getDeclarationDiagnostics()); + } + + if (preEmitDiagnostics.length > 0) { + performance.endSection("beforeTransform"); + return { diagnostics: preEmitDiagnostics, transpiledFiles }; + } + } + + for (const plugin of plugins) { + if (plugin.beforeTransform) { + const pluginDiagnostics = plugin.beforeTransform(program, options, emitHost) ?? []; + diagnostics.push(...pluginDiagnostics); + } + } + + const visitorMap = createVisitorMap(plugins.map(p => p.visitors).filter(isNonNull)); + const printer = createPrinter(plugins.map(p => p.printer).filter(isNonNull)); + + const processSourceFile = (sourceFile: ts.SourceFile) => { + if (options.tstlVerbose) { + console.log(`Transforming ${sourceFile.fileName}`); + } + + performance.startSection("transpile"); + + const { file, diagnostics: transformDiagnostics } = transformSourceFile(program, sourceFile, visitorMap); + diagnostics.push(...transformDiagnostics); + + performance.endSection("transpile"); + + if (!options.noEmit && !options.emitDeclarationOnly) { + performance.startSection("print"); + if (options.tstlVerbose) { + console.log(`Printing ${sourceFile.fileName}`); + } + + const printResult = printer(program, emitHost, sourceFile.fileName, file); + transpiledFiles.push({ + sourceFiles: [sourceFile], + fileName: path.normalize(sourceFile.fileName), + luaAst: file, + ...printResult, + }); + performance.endSection("print"); + } + }; + + const transformers = getTransformers(program, diagnostics, customTransformers, processSourceFile); + + const isEmittableJsonFile = (sourceFile: ts.SourceFile) => + sourceFile.flags & ts.NodeFlags.JsonFile && + !options.emitDeclarationOnly && + !program.isSourceFileFromExternalLibrary(sourceFile); + + // We always have to run transformers to get diagnostics + const oldNoEmit = options.noEmit; + options.noEmit = false; + + const writeFile: ts.WriteFileCallback = (fileName, ...rest) => { + if (!fileName.endsWith(".js") && !fileName.endsWith(".js.map") && !fileName.endsWith(".json")) { + writeFileResult(fileName, ...rest); + } + }; + + performance.endSection("beforeTransform"); + + if (targetSourceFiles) { + for (const file of targetSourceFiles) { + if (isEmittableJsonFile(file)) { + processSourceFile(file); + } else { + diagnostics.push(...program.emit(file, writeFile, undefined, false, transformers).diagnostics); + } + } + } else { + diagnostics.push(...program.emit(undefined, writeFile, undefined, false, transformers).diagnostics); + + // JSON files don't get through transformers and aren't written when outDir is the same as rootDir + program.getSourceFiles().filter(isEmittableJsonFile).forEach(processSourceFile); + } + + performance.startSection("afterPrint"); + + options.noEmit = oldNoEmit; + + if (options.noEmit || (options.noEmitOnError && diagnostics.length > 0)) { + transpiledFiles = []; + } + + for (const plugin of plugins) { + if (plugin.afterPrint) { + const pluginDiagnostics = plugin.afterPrint(program, options, emitHost, transpiledFiles) ?? []; + diagnostics.push(...pluginDiagnostics); + } + } + + performance.endSection("afterPrint"); + + return { diagnostics, transpiledFiles }; +} diff --git a/src/transpilation/transpiler.ts b/src/transpilation/transpiler.ts new file mode 100644 index 000000000..9bac1f5bf --- /dev/null +++ b/src/transpilation/transpiler.ts @@ -0,0 +1,226 @@ +import * as path from "path"; +import * as ts from "typescript"; +import { CompilerOptions, isBundleEnabled, LuaLibImportKind, LuaTarget } from "../CompilerOptions"; +import { buildMinimalLualibBundle, findUsedLualibFeatures, getLuaLibBundle } from "../LuaLib"; +import { normalizeSlashes, trimExtension } from "../utils"; +import { getBundleResult } from "./bundle"; +import { getPlugins, Plugin } from "./plugins"; +import { resolveDependencies } from "./resolve"; +import { getProgramTranspileResult, TranspileOptions } from "./transpile"; +import { EmitFile, EmitHost, ProcessedFile } from "./utils"; +import * as performance from "../measure-performance"; + +export interface TranspilerOptions { + emitHost?: EmitHost; +} + +export interface EmitOptions extends TranspileOptions { + writeFile?: ts.WriteFileCallback; +} + +export interface EmitResult { + emitSkipped: boolean; + diagnostics: readonly ts.Diagnostic[]; +} + +export class Transpiler { + protected emitHost: EmitHost; + + constructor({ emitHost = ts.sys }: TranspilerOptions = {}) { + this.emitHost = emitHost; + } + + public emit(emitOptions: EmitOptions): EmitResult { + const { program, writeFile = this.emitHost.writeFile, plugins: optionsPlugins = [] } = emitOptions; + + const { diagnostics: getPluginsDiagnostics, plugins: configPlugins } = getPlugins(program); + const plugins = [...optionsPlugins, ...configPlugins]; + + const { diagnostics: transpileDiagnostics, transpiledFiles: freshFiles } = getProgramTranspileResult( + this.emitHost, + writeFile, + { + ...emitOptions, + plugins, + } + ); + + const { emitPlan } = this.getEmitPlan(program, transpileDiagnostics, freshFiles, plugins); + + const emitDiagnostics = this.emitFiles(program, plugins, emitPlan, writeFile); + + return { + diagnostics: getPluginsDiagnostics.concat(transpileDiagnostics, emitDiagnostics), + emitSkipped: emitPlan.length === 0, + }; + } + + private emitFiles( + program: ts.Program, + plugins: Plugin[], + emitPlan: EmitFile[], + writeFile: ts.WriteFileCallback + ): ts.Diagnostic[] { + performance.startSection("emit"); + + const options = program.getCompilerOptions() as CompilerOptions; + + if (options.tstlVerbose) { + console.log("Emitting output"); + } + + const diagnostics: ts.Diagnostic[] = []; + + for (const plugin of plugins) { + if (plugin.beforeEmit) { + const beforeEmitPluginDiagnostics = plugin.beforeEmit(program, options, this.emitHost, emitPlan) ?? []; + diagnostics.push(...beforeEmitPluginDiagnostics); + } + } + + const emitBOM = options.emitBOM ?? false; + for (const { outputPath, code, sourceMap, sourceFiles } of emitPlan) { + if (options.tstlVerbose) { + console.log(`Emitting ${normalizeSlashes(outputPath)}`); + } + + writeFile(outputPath, code, emitBOM, undefined, sourceFiles); + if (options.sourceMap && sourceMap !== undefined) { + writeFile(outputPath + ".map", sourceMap, emitBOM, undefined, sourceFiles); + } + } + + for (const plugin of plugins) { + if (plugin.afterEmit) { + const afterEmitPluginDiagnostics = plugin.afterEmit(program, options, this.emitHost, emitPlan) ?? []; + diagnostics.push(...afterEmitPluginDiagnostics); + } + } + + if (options.tstlVerbose) { + console.log("Emit finished!"); + } + + performance.endSection("emit"); + + return diagnostics; + } + + protected getEmitPlan( + program: ts.Program, + diagnostics: ts.Diagnostic[], + files: ProcessedFile[], + plugins: Plugin[] + ): { emitPlan: EmitFile[] } { + performance.startSection("getEmitPlan"); + const options = program.getCompilerOptions() as CompilerOptions; + + if (options.tstlVerbose) { + console.log("Constructing emit plan"); + } + + // Resolve imported modules and modify output Lua requires + const resolutionResult = resolveDependencies(program, files, this.emitHost, plugins); + diagnostics.push(...resolutionResult.diagnostics); + + const lualibRequired = resolutionResult.resolvedFiles.some(f => f.fileName === "lualib_bundle"); + if (lualibRequired) { + // Remove lualib placeholders from resolution result + resolutionResult.resolvedFiles = resolutionResult.resolvedFiles.filter(f => f.fileName !== "lualib_bundle"); + + if (options.tstlVerbose) { + console.log("Including lualib bundle"); + } + // Add lualib bundle to source dir 'virtually', will be moved to correct output dir in emitPlan + const fileName = normalizeSlashes(path.resolve(getSourceDir(program), "lualib_bundle.lua")); + const code = this.getLuaLibBundleContent(options, resolutionResult.resolvedFiles); + resolutionResult.resolvedFiles.unshift({ fileName, code }); + } + + let emitPlan: EmitFile[]; + if (isBundleEnabled(options)) { + const [bundleDiagnostics, bundleFile] = getBundleResult(program, resolutionResult.resolvedFiles); + diagnostics.push(...bundleDiagnostics); + emitPlan = [bundleFile]; + } else { + emitPlan = resolutionResult.resolvedFiles.map(file => ({ + ...file, + outputPath: getEmitPath(file.fileName, program), + })); + } + + performance.endSection("getEmitPlan"); + + return { emitPlan }; + } + + private getLuaLibBundleContent(options: CompilerOptions, resolvedFiles: ProcessedFile[]) { + const luaTarget = options.luaTarget ?? LuaTarget.Universal; + if (options.luaLibImport === LuaLibImportKind.RequireMinimal) { + const usedFeatures = findUsedLualibFeatures( + luaTarget, + this.emitHost, + resolvedFiles.map(f => f.code) + ); + return buildMinimalLualibBundle(usedFeatures, luaTarget, this.emitHost); + } else { + return getLuaLibBundle(luaTarget, this.emitHost); + } + } +} + +export function getEmitPath(file: string, program: ts.Program): string { + const relativeOutputPath = getEmitPathRelativeToOutDir(file, program); + const outDir = getEmitOutDir(program); + + return path.join(outDir, relativeOutputPath); +} + +export function getEmitPathRelativeToOutDir(fileName: string, program: ts.Program): string { + const sourceDir = getSourceDir(program); + // Default output path is relative path in source dir + let emitPathSplits = path.relative(sourceDir, fileName).split(path.sep); + + // If source is in a parent directory of source dir, move it into the source dir + emitPathSplits = emitPathSplits.filter(s => s !== ".."); + + // To avoid overwriting lua sources in node_modules, emit into lua_modules + if (emitPathSplits[0] === "node_modules") { + emitPathSplits[0] = "lua_modules"; + } + + // 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; + + return path.join(...emitPathSplits); +} + +export function getSourceDir(program: ts.Program): string { + const rootDir = program.getCompilerOptions().rootDir; + if (rootDir && rootDir.length > 0) { + return path.isAbsolute(rootDir) ? rootDir : path.resolve(getProjectRoot(program), rootDir); + } + + // If no rootDir is given, source is relative to the project root + return getProjectRoot(program); +} + +export function getEmitOutDir(program: ts.Program): string { + const outDir = program.getCompilerOptions().outDir; + if (outDir && outDir.length > 0) { + return path.isAbsolute(outDir) ? outDir : path.resolve(getProjectRoot(program), outDir); + } + + // If no outDir is provided, emit in project root + return getProjectRoot(program); +} + +export function getProjectRoot(program: ts.Program): string { + // Try to get the directory the tsconfig is in + const tsConfigPath = program.getCompilerOptions().configFilePath; + // If no tsconfig is known, use common source directory + return tsConfigPath ? path.dirname(tsConfigPath) : program.getCommonSourceDirectory(); +} diff --git a/src/transpilation/utils.ts b/src/transpilation/utils.ts new file mode 100644 index 000000000..ce03fd854 --- /dev/null +++ b/src/transpilation/utils.ts @@ -0,0 +1,80 @@ +import * as path from "path"; +import * as resolve from "resolve"; +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +// TODO: Don't depend on CLI? +import * as cliDiagnostics from "../cli/diagnostics"; +import * as lua from "../LuaAST"; +import * as diagnosticFactories from "./diagnostics"; + +export interface EmitHost { + directoryExists(path: string): boolean; + fileExists(path: string): boolean; + getCurrentDirectory(): string; + readFile(path: string): string | undefined; + writeFile: ts.WriteFileCallback; +} + +interface BaseFile { + code: string; + sourceMap?: string; + sourceFiles?: ts.SourceFile[]; +} + +export interface ProcessedFile extends BaseFile { + fileName: string; + luaAst?: lua.File; + sourceMapNode?: SourceNode; +} + +export interface EmitFile extends BaseFile { + outputPath: string; +} + +export const getConfigDirectory = (options: ts.CompilerOptions) => + options.configFilePath ? path.dirname(options.configFilePath) : process.cwd(); + +const getTstlDirectory = () => path.dirname(__dirname); + +export function resolvePlugin( + kind: string, + optionName: string, + basedir: string, + query: unknown, + importName = "default" +): { error?: ts.Diagnostic; result?: unknown } { + if (typeof query !== "string") { + return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "string") }; + } + + const isModuleNotFoundError = (error: any) => error.code === "MODULE_NOT_FOUND"; + + let resolved: string; + try { + resolved = resolve.sync(query, { basedir, extensions: [".js", ".ts", ".tsx"] }); + } catch (err) { + if (!isModuleNotFoundError(err)) throw err; + return { error: diagnosticFactories.couldNotResolveFrom(kind, query, basedir) }; + } + + const hasNoRequireHook = require.extensions[".ts"] === undefined; + if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) { + try { + const tsNodePath = resolve.sync("ts-node", { basedir: getTstlDirectory() }); + const tsNode: typeof import("ts-node") = require(tsNodePath); + tsNode.register({ transpileOnly: true }); + } catch (err) { + if (!isModuleNotFoundError(err)) throw err; + return { error: diagnosticFactories.toLoadItShouldBeTranspiled(kind, query) }; + } + } + + const commonjsModule = require(resolved); + const factoryModule = commonjsModule.__esModule ? commonjsModule : { default: commonjsModule }; + const result = factoryModule[importName]; + if (result === undefined) { + return { error: diagnosticFactories.shouldHaveAExport(kind, query, importName) }; + } + + return { result }; +} diff --git a/src/tstl.ts b/src/tstl.ts index c4dd2be38..813347a21 100644 --- a/src/tstl.ts +++ b/src/tstl.ts @@ -1,5 +1,237 @@ #!/usr/bin/env node +import * as ts from "typescript"; +import * as tstl from "."; +import * as cliDiagnostics from "./cli/diagnostics"; +import { getHelpString, versionString } from "./cli/information"; +import { parseCommandLine } from "./cli/parse"; +import { createDiagnosticReporter } from "./cli/report"; +import { createConfigFileUpdater, locateConfigFile, parseConfigFileWithSystem } from "./cli/tsconfig"; +import { isBundleEnabled } from "./CompilerOptions"; +import * as performance from "./measure-performance"; -import {compile} from "./Compiler"; +const shouldBePretty = ({ pretty }: ts.CompilerOptions = {}) => + pretty !== undefined ? (pretty as boolean) : ts.sys.writeOutputIsTTY?.() ?? false; -compile(process.argv.slice(2)); +let reportDiagnostic = createDiagnosticReporter(false); +function updateReportDiagnostic(options?: ts.CompilerOptions): void { + reportDiagnostic = createDiagnosticReporter(shouldBePretty(options)); +} + +function createWatchStatusReporter(options?: ts.CompilerOptions): ts.WatchStatusReporter { + return ts.createWatchStatusReporter(ts.sys, shouldBePretty(options)); +} + +function executeCommandLine(args: string[]): void { + if (args.length > 0 && args[0].startsWith("-")) { + const firstOption = args[0].slice(args[0].startsWith("--") ? 2 : 1).toLowerCase(); + if (firstOption === "build" || firstOption === "b") { + return performBuild(args.slice(1)); + } + } + + const commandLine = parseCommandLine(args); + + if (commandLine.options.build) { + reportDiagnostic(cliDiagnostics.optionBuildMustBeFirstCommandLineArgument()); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + // TODO: ParsedCommandLine.errors isn't meant to contain warnings. Once root-level options + // support would be dropped it should be changed to `commandLine.errors.length > 0`. + if (commandLine.errors.some(e => e.category === ts.DiagnosticCategory.Error)) { + commandLine.errors.forEach(reportDiagnostic); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + if (commandLine.options.version) { + console.log(versionString); + return ts.sys.exit(ts.ExitStatus.Success); + } + + if (commandLine.options.help) { + console.log(versionString); + console.log(getHelpString()); + return ts.sys.exit(ts.ExitStatus.Success); + } + + const configFileName = locateConfigFile(commandLine); + if (typeof configFileName === "object") { + reportDiagnostic(configFileName); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + const commandLineOptions = commandLine.options; + if (configFileName) { + const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions); + + updateReportDiagnostic(configParseResult.options); + if (configParseResult.options.watch) { + createWatchOfConfigFile(configFileName, commandLineOptions); + } else { + performCompilation( + configParseResult.fileNames, + configParseResult.projectReferences, + configParseResult.options, + ts.getConfigFileParsingDiagnostics(configParseResult) + ); + } + } else { + updateReportDiagnostic(commandLineOptions); + if (commandLineOptions.watch) { + createWatchOfFilesAndCompilerOptions(commandLine.fileNames, commandLineOptions); + } else { + performCompilation(commandLine.fileNames, commandLine.projectReferences, commandLineOptions); + } + } +} + +function performBuild(_args: string[]): void { + console.log("Option '--build' is not supported."); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); +} + +function performCompilation( + rootNames: string[], + projectReferences: readonly ts.ProjectReference[] | undefined, + options: tstl.CompilerOptions, + configFileParsingDiagnostics?: readonly ts.Diagnostic[] +): void { + if (options.measurePerformance) performance.enableMeasurement(); + + performance.startSection("createProgram"); + + const program = ts.createProgram({ + rootNames, + options, + projectReferences, + configFileParsingDiagnostics, + }); + const preEmitDiagnostics = ts.getPreEmitDiagnostics(program); + + performance.endSection("createProgram"); + + const { diagnostics: transpileDiagnostics, emitSkipped } = new tstl.Transpiler().emit({ program }); + + const diagnostics = ts.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics]); + diagnostics.forEach(reportDiagnostic); + + if (options.measurePerformance) reportPerformance(); + + const exitCode = + diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error).length === 0 + ? ts.ExitStatus.Success + : emitSkipped + ? ts.ExitStatus.DiagnosticsPresent_OutputsSkipped + : ts.ExitStatus.DiagnosticsPresent_OutputsGenerated; + + return ts.sys.exit(exitCode); +} + +function createWatchOfConfigFile(configFileName: string, optionsToExtend: tstl.CompilerOptions): void { + const watchCompilerHost = ts.createWatchCompilerHost( + configFileName, + optionsToExtend, + ts.sys, + ts.createSemanticDiagnosticsBuilderProgram, + undefined, + createWatchStatusReporter(optionsToExtend) + ); + + updateWatchCompilationHost(watchCompilerHost, optionsToExtend); + ts.createWatchProgram(watchCompilerHost); +} + +function createWatchOfFilesAndCompilerOptions(rootFiles: string[], options: tstl.CompilerOptions): void { + const watchCompilerHost = ts.createWatchCompilerHost( + rootFiles, + options, + ts.sys, + ts.createSemanticDiagnosticsBuilderProgram, + undefined, + createWatchStatusReporter(options) + ); + + updateWatchCompilationHost(watchCompilerHost, options); + ts.createWatchProgram(watchCompilerHost); +} + +function updateWatchCompilationHost( + host: ts.WatchCompilerHost, + optionsToExtend: tstl.CompilerOptions +): void { + let hadErrorLastTime = true; + const updateConfigFile = createConfigFileUpdater(optionsToExtend); + + const transpiler = new tstl.Transpiler(); + host.afterProgramCreate = builderProgram => { + const program = builderProgram.getProgram(); + const options = builderProgram.getCompilerOptions() as tstl.CompilerOptions; + + if (options.measurePerformance) performance.enableMeasurement(); + + const configFileParsingDiagnostics: ts.Diagnostic[] = updateConfigFile(options); + + let sourceFiles: ts.SourceFile[] | undefined; + if (!isBundleEnabled(options) && !hadErrorLastTime) { + sourceFiles = []; + while (true) { + const currentFile = builderProgram.getSemanticDiagnosticsOfNextAffectedFile(); + if (!currentFile) break; + + if ("fileName" in currentFile.affected) { + sourceFiles.push(currentFile.affected); + } else { + sourceFiles.push(...currentFile.affected.getSourceFiles()); + } + } + } + + const { diagnostics: emitDiagnostics } = transpiler.emit({ program, sourceFiles }); + + const diagnostics = ts.sortAndDeduplicateDiagnostics([ + ...configFileParsingDiagnostics, + ...program.getOptionsDiagnostics(), + ...program.getSyntacticDiagnostics(), + ...program.getGlobalDiagnostics(), + ...program.getSemanticDiagnostics(), + ...emitDiagnostics, + ]); + + diagnostics.forEach(reportDiagnostic); + + if (options.measurePerformance) reportPerformance(); + + const errors = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); + hadErrorLastTime = errors.length > 0; + + host.onWatchStatusChange!(cliDiagnostics.watchErrorSummary(errors.length), host.getNewLine(), options); + }; +} + +function reportPerformance() { + if (performance.isMeasurementEnabled()) { + console.log("Performance measurements: "); + performance.forEachMeasure((name, duration) => { + console.log(` ${name}: ${duration.toFixed(2)}ms`); + }); + console.log(`Total: ${performance.getTotalDuration().toFixed(2)}ms`); + performance.disableMeasurement(); + } +} + +function checkNodeVersion(): void { + const [major, minor] = process.version.slice(1).split(".").map(Number); + const isValid = major > 12 || (major === 12 && minor >= 13); + if (!isValid) { + console.error(`TypeScriptToLua requires Node.js >=12.13.0, the current version is ${process.version}`); + process.exit(1); + } +} + +checkNodeVersion(); + +if (ts.sys.setBlocking) { + ts.sys.setBlocking(); +} + +executeCommandLine(ts.sys.args); diff --git a/src/typescript-internal.d.ts b/src/typescript-internal.d.ts new file mode 100644 index 000000000..5333df95a --- /dev/null +++ b/src/typescript-internal.d.ts @@ -0,0 +1,62 @@ +export {}; + +declare module "typescript" { + function createDiagnosticReporter(system: System, pretty?: boolean): DiagnosticReporter; + function createWatchStatusReporter(system: System, pretty?: boolean): WatchStatusReporter; + + interface System { + setBlocking?(): void; + } + + interface Statement { + jsDoc?: JSDoc[]; + } + + interface Program { + getCommonSourceDirectory(): string; + } + + interface CompilerOptions { + configFile?: TsConfigSourceFile; + configFilePath?: string; + } + + interface TypeChecker { + getElementTypeOfArrayType(type: Type): Type | undefined; + getContextualTypeForObjectLiteralElement(element: ObjectLiteralElementLike): Type | undefined; + + isTupleType(type: Type): boolean; + isArrayType(type: Type): boolean; + } + + interface Symbol { + parent?: Symbol; + } + + interface Signature { + compositeSignatures?: Signature[]; + } + + function transformJsx(context: TransformationContext): (x: SourceFile) => SourceFile; + + export type OuterExpression = + | ParenthesizedExpression + | TypeAssertion + | AsExpression + | NonNullExpression + | PartiallyEmittedExpression; + + function skipOuterExpressions(node: Expression, kinds?: OuterExpressionKinds): Expression; + export function isOuterExpression(node: Node, kinds?: OuterExpressionKinds): node is OuterExpression; + + export function nodeNextJsonConfigResolver( + moduleName: string, + containingFile: string, + host: ModuleResolutionHost + ): ResolvedModuleWithFailedLookupLocations; + + export function pathIsAbsolute(path: string): boolean; + export function pathIsRelative(path: string): boolean; + + export function setParent(child: T, parent: T["parent"] | undefined): T; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..b1877e368 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,87 @@ +import * as ts from "typescript"; +import * as nativeAssert from "assert"; +import * as path from "path"; + +export function castArray(value: T | T[]): T[]; +export function castArray(value: T | readonly T[]): readonly T[]; +export function castArray(value: T | readonly T[]): readonly T[] { + return Array.isArray(value) ? value : [value as T]; +} + +export const intersperse = (values: readonly T[], separator: T): T[] => + values.flatMap((value, index) => (index === 0 ? [value] : [separator, value])); + +export const union = (...values: Array>): T[] => [...new Set(...values)]; + +export const intersection = (first: readonly T[], ...rest: Array): T[] => + union(first).filter(x => rest.every(r => r.includes(x))); + +type DiagnosticFactory = (...args: any) => Partial & Pick; +export const createDiagnosticFactoryWithCode = (code: number, create: T) => + Object.assign( + (...args: Parameters): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Error, + code, + source: "typescript-to-lua", + ...create(...(args as any)), + }), + { code } + ); + +let serialDiagnosticCodeCounter = 100000; +export const createSerialDiagnosticFactory = (create: T) => + createDiagnosticFactoryWithCode(serialDiagnosticCodeCounter++, create); + +export const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); +export const trimExtension = (filePath: string) => filePath.slice(0, -path.extname(filePath).length); + +export function formatPathToLuaPath(filePath: string): string { + filePath = filePath.replace(/\.json$/, ""); + if (process.platform === "win32") { + // Windows can use backslashes + filePath = filePath.replace(/\.\\/g, "").replace(/\\/g, "."); + } + return filePath.replace(/\.\//g, "").replace(/\//g, "."); +} + +type NoInfer = [T][T extends any ? 0 : never]; + +export function getOrUpdate( + map: Map | (K extends object ? WeakMap : never), + key: K, + getDefaultValue: () => NoInfer +): V { + if (!map.has(key)) { + map.set(key, getDefaultValue()); + } + + return map.get(key)!; +} + +export function isNonNull(value: T | null | undefined): value is T { + return value != null; +} + +export function cast( + item: TOriginal, + cast: (item: TOriginal) => item is TCast +): TCast { + if (cast(item)) { + return item; + } else { + throw new Error(`Failed to cast value to expected type using ${cast.name}.`); + } +} + +export function assert(value: any, message?: string | Error): asserts value { + nativeAssert(value, message); +} + +export function assertNever(_value: never): never { + throw new Error("Value is expected to be never"); +} + +export function assume(_value: any): asserts _value is T {} diff --git a/test/cli/errors.spec.ts b/test/cli/errors.spec.ts new file mode 100644 index 000000000..8ff45ce14 --- /dev/null +++ b/test/cli/errors.spec.ts @@ -0,0 +1,27 @@ +import * as fs from "fs"; +import * as path from "path"; +import { runCli } from "./run"; + +const srcFilePath = path.resolve(__dirname, "errors", "error.ts"); +const outFilePath = path.resolve(__dirname, "errors", "error.lua"); +const errorMessage = "Unable to convert function with no 'this' parameter to function with 'this'."; + +afterEach(() => { + if (fs.existsSync(outFilePath)) fs.unlinkSync(outFilePath); +}); + +test("should report errors", async () => { + const { exitCode, output } = await runCli([srcFilePath]); + + expect(output).toContain(errorMessage); + expect(exitCode).toBe(2); + expect(fs.existsSync(outFilePath)).toBe(true); +}); + +test("shouldn't emit files with --noEmitOnError", async () => { + const { exitCode, output } = await runCli(["--noEmitOnError", srcFilePath]); + + expect(output).toContain(errorMessage); + expect(exitCode).toBe(1); + expect(fs.existsSync(outFilePath)).toBe(false); +}); diff --git a/test/cli/errors/error.ts b/test/cli/errors/error.ts new file mode 100644 index 000000000..6b4c5320c --- /dev/null +++ b/test/cli/errors/error.ts @@ -0,0 +1,2 @@ +const foo: (this: void) => void = () => {}; +const bar: () => void = foo; diff --git a/test/cli/parse.spec.ts b/test/cli/parse.spec.ts new file mode 100644 index 000000000..c33af1208 --- /dev/null +++ b/test/cli/parse.spec.ts @@ -0,0 +1,270 @@ +import * as ts from "typescript"; +import * as tstl from "../../src"; + +describe("command line", () => { + test("should support aliases", () => { + const full = tstl.parseCommandLine(["--luaTarget", "5.1"]); + const alias = tstl.parseCommandLine(["-lt", "5.1"]); + expect(full).toEqual(alias); + }); + + test("should support standard typescript options", () => { + const commandLine = "main.ts --project tsconfig.json --noHeader -t es3 -lt 5.3"; + const result = tstl.parseCommandLine(commandLine.split(" ")); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.fileNames).toEqual(["main.ts"]); + expect(result.options).toEqual({ + project: "tsconfig.json", + noHeader: true, + target: ts.ScriptTarget.ES3, + luaTarget: tstl.LuaTarget.Lua53, + }); + }); + + test("should error on unknown options", () => { + const result = tstl.parseCommandLine(["--unknownOption"]); + + expect(result.errors).toHaveDiagnostics(); + }); + + test("should parse options case-insensitively", () => { + const result = tstl.parseCommandLine(["--NoHeader"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + }); + + describe("enum options", () => { + test("should parse enums", () => { + const result = tstl.parseCommandLine(["--luaTarget", "5.1"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.Lua51); + }); + + test("should be case-insensitive", () => { + for (const value of ["jit", "JiT", "JIT"]) { + const result = tstl.parseCommandLine(["--luaTarget", value]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.LuaJIT); + } + }); + + test("should error on invalid value", () => { + const result = tstl.parseCommandLine(["--luaTarget", "invalid"]); + + expect(result.errors).toHaveDiagnostics(); + }); + }); + + describe("array-of-objects options", () => { + test.each([["{"], ["{}"], ["0"], ["''"]])("should error on invalid value (%s)", value => { + const result = tstl.parseCommandLine(["--luaPlugins", value]); + + expect(result.errors).toHaveDiagnostics(); + }); + }); + + describe("boolean options", () => { + test.each([true, false])("should parse booleans (%p)", value => { + const result = tstl.parseCommandLine(["--noHeader", value.toString()]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(value); + }); + + test("should be case-sensitive", () => { + const result = tstl.parseCommandLine(["--noHeader", "FALSE"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + expect(result.fileNames).toEqual(["FALSE"]); + }); + + test("should be parsed without a value", () => { + const result = tstl.parseCommandLine(["--noHeader"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + }); + + test("shouldn't parse following arguments as values", () => { + const result = tstl.parseCommandLine(["--noHeader", "--noImplicitSelf"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + expect(result.options.noImplicitSelf).toBe(true); + }); + + test("shouldn't parse following files as values", () => { + const result = tstl.parseCommandLine(["--noHeader", "file.ts"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + }); + }); + + describe("integration", () => { + test.each<[string, string, tstl.CompilerOptions]>([ + ["noHeader", "false", { noHeader: false }], + ["noHeader", "true", { noHeader: true }], + ["sourceMapTraceback", "false", { sourceMapTraceback: false }], + ["sourceMapTraceback", "true", { sourceMapTraceback: true }], + ["tstlVerbose", "true", { tstlVerbose: true }], + ["tstlVerbose", "false", { tstlVerbose: false }], + + ["buildMode", "default", { buildMode: tstl.BuildMode.Default }], + ["buildMode", "library", { buildMode: tstl.BuildMode.Library }], + + ["luaLibImport", "none", { luaLibImport: tstl.LuaLibImportKind.None }], + ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], + ["luaLibImport", "require", { luaLibImport: tstl.LuaLibImportKind.Require }], + + ["luaTarget", "universal", { luaTarget: tstl.LuaTarget.Universal }], + ["luaTarget", "5.1", { luaTarget: tstl.LuaTarget.Lua51 }], + ["luaTarget", "5.2", { luaTarget: tstl.LuaTarget.Lua52 }], + ["luaTarget", "5.3", { luaTarget: tstl.LuaTarget.Lua53 }], + ["luaTarget", "jit", { luaTarget: tstl.LuaTarget.LuaJIT }], + + ["luaBundle", "foo", { luaBundle: "foo" }], + ["luaBundleEntry", "bar", { luaBundleEntry: "bar" }], + + ["extension", ".lua", { extension: ".lua" }], + ["extension", "scar", { extension: "scar" }], + + ["luaPlugins", '[{ "name": "a" }]', { luaPlugins: [{ name: "a" }] }], + ["luaPlugins", '[{ "name": "a" },{ "name": "b" }]', { luaPlugins: [{ name: "a" }, { name: "b" }] }], + + ["noResolvePaths", "path1", { noResolvePaths: ["path1"] }], + ["noResolvePaths", "path1,path2", { noResolvePaths: ["path1", "path2"] }], + ])("--%s %s", (optionName, value, expected) => { + const result = tstl.parseCommandLine([`--${optionName}`, value]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options).toEqual(expected); + }); + }); +}); + +describe("tsconfig", () => { + const parseConfigFileContent = (config: any) => { + // Specifying `files` option disables automatic file searching, that includes all files in + // the project, making these tests slow. Empty file list is considered as an error. + config.files = ["src/index.ts"]; + return tstl.updateParsedConfigFile(ts.parseJsonConfigFileContent(config, ts.sys, "")); + }; + + test("should support deprecated root-level options", () => { + const rootLevel = parseConfigFileContent({ noHeader: true }); + const namespaced = parseConfigFileContent({ tstl: { noHeader: true } }); + + expect(rootLevel.errors).toEqual([expect.objectContaining({ category: ts.DiagnosticCategory.Warning })]); + expect(rootLevel.options).toEqual(namespaced.options); + }); + + test("should allow unknown root-level options", () => { + const result = parseConfigFileContent({ unknownOption: true }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.unknownOption).toBeUndefined(); + }); + + test("should error on unknown namespaced options", () => { + const result = parseConfigFileContent({ tstl: { unknownOption: true } }); + + expect(result.errors).toHaveDiagnostics(); + expect(result.options.unknownOption).toBeUndefined(); + }); + + test("should parse options case-sensitively", () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const result = parseConfigFileContent({ tstl: { NoHeader: true } }); + + expect(result.errors).toHaveDiagnostics(); + expect(result.options.NoHeader).toBeUndefined(); + expect(result.options.noHeader).toBeUndefined(); + }); + + describe("enum options", () => { + test("should parse enums", () => { + const result = parseConfigFileContent({ tstl: { luaTarget: "5.1" } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.Lua51); + }); + + test("should be case-insensitive", () => { + for (const value of ["jit", "JiT", "JIT"]) { + const result = parseConfigFileContent({ tstl: { luaTarget: value } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.LuaJIT); + } + }); + + test("should error on invalid value", () => { + const result = parseConfigFileContent({ tstl: { luaTarget: "invalid" } }); + + expect(result.errors).toHaveDiagnostics(); + }); + }); + + describe("boolean options", () => { + test.each([true, false])("should parse booleans (%p)", value => { + const result = parseConfigFileContent({ tstl: { noHeader: value } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(value); + }); + + test("shouldn't parse strings", () => { + const result = parseConfigFileContent({ tstl: { noHeader: "true" } }); + + expect(result.errors).toHaveDiagnostics(); + expect(result.options.noHeader).toBeUndefined(); + }); + }); + + describe("integration", () => { + test.each<[string, any, tstl.CompilerOptions]>([ + ["noHeader", false, { noHeader: false }], + ["noHeader", true, { noHeader: true }], + ["sourceMapTraceback", false, { sourceMapTraceback: false }], + ["sourceMapTraceback", true, { sourceMapTraceback: true }], + + ["buildMode", "default", { buildMode: tstl.BuildMode.Default }], + ["buildMode", "library", { buildMode: tstl.BuildMode.Library }], + + ["luaLibImport", "none", { luaLibImport: tstl.LuaLibImportKind.None }], + ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], + ["luaLibImport", "require", { luaLibImport: tstl.LuaLibImportKind.Require }], + + ["luaTarget", "universal", { luaTarget: tstl.LuaTarget.Universal }], + ["luaTarget", "5.0", { luaTarget: tstl.LuaTarget.Lua50 }], + ["luaTarget", "5.1", { luaTarget: tstl.LuaTarget.Lua51 }], + ["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" }], + + ["noImplicitSelf", true, { noImplicitSelf: true }], + + ["tstlVerbose", true, { tstlVerbose: true }], + + ["noResolvePaths", [], { noResolvePaths: [] }], + ["noResolvePaths", ["path1", "path2"], { noResolvePaths: ["path1", "path2"] }], + ])("{ %p: %p }", (optionName, value, expected) => { + const result = parseConfigFileContent({ tstl: { [optionName]: value } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options).toEqual(expected); + }); + }); +}); diff --git a/test/cli/run.ts b/test/cli/run.ts new file mode 100644 index 000000000..732d71bcb --- /dev/null +++ b/test/cli/run.ts @@ -0,0 +1,31 @@ +import { ChildProcess, fork } from "child_process"; +import * as path from "path"; + +jest.setTimeout(30000); + +const cliPath = path.join(__dirname, "../../src/tstl.ts"); + +const defaultArgs = ["--skipLibCheck", "--types", "node"]; +export function forkCli(args: string[]): ChildProcess { + return fork(cliPath, [...defaultArgs, ...args], { + stdio: "pipe", + execArgv: ["--require", "ts-node/register/transpile-only"], + }); +} + +export interface CliResult { + exitCode: number; + output: string; +} + +export async function runCli(args: string[]): Promise { + const child = forkCli(args); + + let output = ""; + child.stdout!.on("data", (data: Buffer) => (output += data.toString())); + child.stderr!.on("data", (data: Buffer) => (output += data.toString())); + + return new Promise(resolve => { + child.on("close", exitCode => resolve({ exitCode: exitCode ?? 1, output })); + }); +} diff --git a/test/cli/tsconfig.spec.ts b/test/cli/tsconfig.spec.ts new file mode 100644 index 000000000..e5d1b946c --- /dev/null +++ b/test/cli/tsconfig.spec.ts @@ -0,0 +1,115 @@ +import * as fs from "fs-extra"; +import * as os from "os"; +import * as path from "path"; +import { locateConfigFile, parseConfigFileWithSystem } from "../../src/cli/tsconfig"; +import { normalizeSlashes } from "../../src/utils"; + +let temp: string; +beforeEach(async () => { + temp = await fs.mkdtemp(path.join(os.tmpdir(), "tstl-test-")); + process.chdir(temp); +}); + +const originalWorkingDirectory = process.cwd(); +afterEach(async () => { + process.chdir(originalWorkingDirectory); + // TODO [node@12]: `rmdir` has `recursive` option + await fs.remove(temp); +}); + +const locate = (project: string | undefined, fileNames: string[] = []) => + locateConfigFile({ errors: [], fileNames, options: { project } }); + +const normalize = (name: string) => normalizeSlashes(fs.realpathSync(path.resolve(temp, name))); + +describe("specified", () => { + for (const separator of process.platform === "win32" ? ["/", "\\"] : ["/"]) { + for (const pointsTo of ["file", "directory"] as const) { + const findAndExpect = (project: string, expected: string) => { + project = project.replace(/[\\/]/g, separator); + if (pointsTo === "directory") { + project = path.dirname(project); + } + + expect(locate(project)).toBe(normalize(expected)); + }; + + test(`relative to ${pointsTo} separated with '${separator}'`, async () => { + await fs.outputFile("tsconfig.json", ""); + await fs.mkdir("src"); + process.chdir("src"); + findAndExpect("../tsconfig.json", "tsconfig.json"); + }); + + test(`absolute to ${pointsTo} separated with '${separator}'`, async () => { + await fs.outputFile("tsconfig.json", ""); + findAndExpect(path.resolve("tsconfig.json"), "tsconfig.json"); + }); + } + } + + test.each(["", ".", "./"])("current directory (%p)", async () => { + await fs.outputFile("tsconfig.json", ""); + expect(locate(".")).toBe(normalize("tsconfig.json")); + }); +}); + +describe("inferred", () => { + test("in current directory", async () => { + await fs.outputFile("tsconfig.json", ""); + expect(locate(undefined)).toBe(normalize("tsconfig.json")); + }); + + test("in parent directory", async () => { + await fs.outputFile("tsconfig.json", ""); + await fs.mkdir("src"); + process.chdir("src"); + expect(locate(undefined)).toBe(normalize("tsconfig.json")); + }); + + test("not found", () => { + expect(locate(undefined)).toBeUndefined(); + }); + + test("does not attempt when has files", async () => { + await fs.outputFile("tsconfig.json", ""); + expect(locate(undefined, [""])).toBeUndefined(); + }); +}); + +describe("errors", () => { + test("specified file does not exist", () => { + expect([locate("tsconfig.json")]).toHaveDiagnostics(); + }); + + test("specified directory does not exist", () => { + expect([locate("project")]).toHaveDiagnostics(); + }); + + test("cannot be mixed", async () => { + await fs.outputFile("tsconfig.json", ""); + expect([locate("tsconfig.json", [""])]).toHaveDiagnostics(); + }); +}); + +describe("tsconfig extends", () => { + test("correctly merges extended tsconfig files", () => { + const parsedConfig = parseConfigFileWithSystem(path.join(__dirname, "tsconfig", "tsconfig.json")); + expect(parsedConfig.options).toMatchObject({ luaTarget: "5.3", noHeader: true }); + }); + + test("can handle multiple extends", () => { + const parsedConfig = parseConfigFileWithSystem(path.join(__dirname, "tsconfig", "tsconfig.multi-extends.json")); + expect(parsedConfig.options).toMatchObject({ luaTarget: "5.4", sourceMapTraceback: true }); + }); + + test("can handle cycles in configs", () => { + const parsedConfig = parseConfigFileWithSystem(path.join(__dirname, "tsconfig", "tsconfig-cycle1.json")); + expect(parsedConfig.options).toMatchObject({ luaTarget: "5.4" }); + }); + + test("can handle tsconfig files with comments", () => { + const parsedConfig = parseConfigFileWithSystem(path.join(__dirname, "tsconfig", "tsconfig.with-comments.json")); + expect(parsedConfig.options).toMatchObject({ luaTarget: "5.3" }); + }); +}); diff --git a/test/cli/tsconfig/tsconfig-cycle1.json b/test/cli/tsconfig/tsconfig-cycle1.json new file mode 100644 index 000000000..6383b5ccf --- /dev/null +++ b/test/cli/tsconfig/tsconfig-cycle1.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig-cycle2.json", + "tstl": { + "luaTarget": "5.4" + } +} diff --git a/test/cli/tsconfig/tsconfig-cycle2.json b/test/cli/tsconfig/tsconfig-cycle2.json new file mode 100644 index 000000000..14c3edcd1 --- /dev/null +++ b/test/cli/tsconfig/tsconfig-cycle2.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig-cycle1.json", + "tstl": { + "luaTarget": "5.3" + } +} diff --git a/test/cli/tsconfig/tsconfig.base.json b/test/cli/tsconfig/tsconfig.base.json new file mode 100644 index 000000000..6b296a8de --- /dev/null +++ b/test/cli/tsconfig/tsconfig.base.json @@ -0,0 +1,6 @@ +{ + "tstl": { + "noHeader": true, + "luaTarget": "jit" + } +} diff --git a/test/cli/tsconfig/tsconfig.json b/test/cli/tsconfig/tsconfig.json new file mode 100644 index 000000000..7e76044a2 --- /dev/null +++ b/test/cli/tsconfig/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.base.json", + "tstl": { + "luaTarget": "5.3" + } +} diff --git a/test/cli/tsconfig/tsconfig.multi-extends.json b/test/cli/tsconfig/tsconfig.multi-extends.json new file mode 100644 index 000000000..7404a6266 --- /dev/null +++ b/test/cli/tsconfig/tsconfig.multi-extends.json @@ -0,0 +1,6 @@ +{ + "extends": ["./tsconfig.json", "./tsconfig2.json"], + "tstl": { + "sourceMapTraceback": true + } +} diff --git a/test/cli/tsconfig/tsconfig.with-comments.json b/test/cli/tsconfig/tsconfig.with-comments.json new file mode 100644 index 000000000..9baf9adda --- /dev/null +++ b/test/cli/tsconfig/tsconfig.with-comments.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "tstl": { + // Can handle comments + /* also of this kind */ + "luaTarget": "5.3" + } +} diff --git a/test/cli/tsconfig/tsconfig2.json b/test/cli/tsconfig/tsconfig2.json new file mode 100644 index 000000000..9cfef77e0 --- /dev/null +++ b/test/cli/tsconfig/tsconfig2.json @@ -0,0 +1,5 @@ +{ + "tstl": { + "luaTarget": "5.4" + } +} diff --git a/test/cli/watch.spec.ts b/test/cli/watch.spec.ts new file mode 100644 index 000000000..35d2f2f59 --- /dev/null +++ b/test/cli/watch.spec.ts @@ -0,0 +1,64 @@ +import * as fs from "fs-extra"; +import * as path from "path"; +import { forkCli } from "./run"; + +let testsCleanup: Array<() => void> = []; +afterEach(() => { + testsCleanup.forEach(x => x()); + testsCleanup = []; +}); + +async function waitForFileExists(filePath: string): Promise { + return new Promise(resolve => { + const intervalTimerId = setInterval(() => { + if (fs.existsSync(filePath)) { + clearInterval(intervalTimerId); + resolve(); + } + }, 100); + + testsCleanup.push(() => clearInterval(intervalTimerId)); + }); +} + +function forkWatchProcess(args: string[]): void { + const child = forkCli(["--watch", ...args]); + testsCleanup.push(() => child.kill()); +} + +const watchedFile = path.join(__dirname, "./watch/watch.ts"); +const watchedFileOut = watchedFile.replace(".ts", ".lua"); + +afterEach(() => fs.removeSync(watchedFileOut)); + +async function compileChangeAndCompare(filePath: string, content: string): Promise { + await waitForFileExists(watchedFileOut); + const initialResultLua = fs.readFileSync(watchedFileOut, "utf8"); + + fs.unlinkSync(watchedFileOut); + + const originalContent = fs.readFileSync(filePath, "utf8"); + fs.writeFileSync(filePath, content); + testsCleanup.push(() => fs.writeFileSync(filePath, originalContent)); + + await waitForFileExists(watchedFileOut); + const updatedResultLua = fs.readFileSync(watchedFileOut, "utf8"); + + expect(initialResultLua).not.toEqual(updatedResultLua); +} + +test("should watch single file", async () => { + forkWatchProcess([path.join(__dirname, "./watch/watch.ts")]); + await compileChangeAndCompare(watchedFile, "const value = 1;"); +}); + +test("should watch project", async () => { + forkWatchProcess(["--project", path.join(__dirname, "./watch")]); + await compileChangeAndCompare(watchedFile, "const value = 1;"); +}); + +test("should watch config file", async () => { + const configFilePath = path.join(__dirname, "./watch/tsconfig.json"); + forkWatchProcess(["--project", configFilePath]); + await compileChangeAndCompare(configFilePath, '{ "tstl": { "luaTarget": "5.3" } }'); +}); diff --git a/test/cli/watch/tsconfig.json b/test/cli/watch/tsconfig.json new file mode 100644 index 000000000..922b8de4c --- /dev/null +++ b/test/cli/watch/tsconfig.json @@ -0,0 +1 @@ +{ "tstl": { "luaTarget": "JIT" } } diff --git a/test/cli/watch/watch.ts b/test/cli/watch/watch.ts new file mode 100644 index 000000000..45813bdf9 --- /dev/null +++ b/test/cli/watch/watch.ts @@ -0,0 +1 @@ +const value = 0 & 0; diff --git a/test/compiler/errorreport.spec.ts b/test/compiler/errorreport.spec.ts deleted file mode 100644 index f5f9c8782..000000000 --- a/test/compiler/errorreport.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Any, Expect, Setup, SpyOn, Teardown, Test, TestCase } from "alsatian"; -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; -import { compile, compileFilesWithOptions } from "../../src/Compiler"; - -export class CompilerErrorReportTests { - private originalStdErr: any; - private originalStdOut: any; - private originalProcessExit: any; - - @TestCase("Encountered error parsing file: Default Imports are not supported, please use named imports instead!\n", - "default_import.ts") - @Test("Compile project") - public compileProject(errorMsg: string, ...fileNames: string[]) { - fileNames = fileNames.map((file) => path.resolve(__dirname, "testfiles", file)); - compileFilesWithOptions(fileNames, {outDir: ".", rootDir: "."}); - - Expect(process.stderr.write).toHaveBeenCalledWith(errorMsg, Any); - - Expect(process.exit).toHaveBeenCalledWith(1); - } - - @Setup - private _spyProcess() { - this.originalProcessExit = process.exit; - this.originalStdOut = process.stdout.write; - this.originalStdErr = process.stderr.write; - - SpyOn(process, "exit").andStub(); - SpyOn(process.stderr, "write").andStub(); - SpyOn(process.stdout, "write").andStub(); - } - - @Teardown - private _resetProcess() { - process.exit = this.originalProcessExit; - process.stdout.write = this.originalStdOut; - process.stderr.write = this.originalStdErr; - } - -} diff --git a/test/compiler/outfile.spec.ts b/test/compiler/outfile.spec.ts deleted file mode 100644 index 1b0fa7a44..000000000 --- a/test/compiler/outfile.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Expect, SetupFixture, Teardown, Test, TestCase } from "alsatian"; -import * as fs from "fs"; -import * as path from "path"; -import { compile } from "../../src/Compiler"; - -export class CompilerOutFileTests { - - private outFileRelPath: string; - private outFileAbsPath: string; - - @SetupFixture - public setupFixture() { - this.outFileRelPath = "./testfiles/out_file.script"; - this.outFileAbsPath = path.join(__dirname, this.outFileRelPath); - } - - @Test("Outfile absoulte path") - public outFileAbsTest() { - // Compile project - compile(["--outFile", this.outFileAbsPath, path.join(__dirname, "./testfiles/out_file.ts")]); - - Expect(fs.existsSync(this.outFileAbsPath)).toBe(true); - } - - @Test("Outfile relative path") - public outFileRelTest() { - // Compile project - compile([ - "--outDir", - __dirname, - "--outFile", - this.outFileRelPath, - path.join(__dirname, "./testfiles/out_file.ts"), - ]); - - Expect(fs.existsSync(this.outFileAbsPath)).toBe(true); - } - - @Teardown - public teardown() { - fs.unlink(this.outFileAbsPath, (err) => { - if (err) { - throw err; - } - }); - } -} diff --git a/test/compiler/project.spec.ts b/test/compiler/project.spec.ts deleted file mode 100644 index 329fa4083..000000000 --- a/test/compiler/project.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Expect, Setup, Teardown, Test, TestCase } from "alsatian"; -import * as fs from "fs"; -import * as path from "path"; -import { compile } from "../../src/Compiler"; - -/** - * Find all files inside a dir, recursively. - */ -function getAllFiles(dir: string): string[] { - return fs.readdirSync(dir).reduce( - (files, file) => { - const name = path.join(dir, file); - const isDirectory = fs.statSync(name).isDirectory(); - return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name]; - }, - [] - ); -} - -export class CompilerProjectTests { - - private existingFiles: string[]; - private filesAfterCompile: string[]; - - @TestCase("basic", - "tsconfig.json", - "lualib_bundle.lua", - "test_src/test_lib/file.lua", - "test_src/main.lua") - @TestCase("basic", - ".", - "lualib_bundle.lua", - "test_src/test_lib/file.lua", - "test_src/main.lua") - @TestCase("basic", - "test_src/main.ts", - "lualib_bundle.lua", - "test_src/test_lib/file.lua", - "test_src/main.lua") - @TestCase("basic", - "tsconfig.outDir.json", - "out_dir/lualib_bundle.lua", - "out_dir/test_src/test_lib/file.lua", - "out_dir/test_src/main.lua") - @TestCase("basic", - "tsconfig.rootDir.json", - "test_src/lualib_bundle.lua", - "test_src/test_lib/file.lua", - "test_src/main.lua") - @TestCase("basic", - "tsconfig.bothDirOptions.json", - "out_dir/lualib_bundle.lua", - "out_dir/test_lib/file.lua", - "out_dir/main.lua") - @TestCase("baseurl", - "tsconfig.json", - "out_dir/lualib_bundle.lua", - "out_dir/test_src/test_lib/nested/lib_file.lua", - "out_dir/test_src/main.lua") - @Test("Compile project") - public compileProject(projectName: string, tsconfig: string, ...expectedFiles: string[]) { - const relPathToProject = path.join("projects", projectName); - - // Setup we cant do this in @setup because we need the projectname - this.existingFiles = getAllFiles(path.resolve(__dirname, relPathToProject)); - this.filesAfterCompile = []; - // Setup End - - const tsconfigPath = path.resolve(__dirname, relPathToProject, tsconfig); - - // Compile project - compile(["-p", tsconfigPath]); - - this.filesAfterCompile = getAllFiles(path.resolve(__dirname, relPathToProject)); - expectedFiles = expectedFiles.map(relPath => path.resolve(__dirname, relPathToProject, relPath)); - expectedFiles.push(...this.existingFiles); - - for (const existingFile of this.filesAfterCompile) { - Expect(expectedFiles).toContain(existingFile); - } - } - - @Teardown - public teardown() { - // Remove files that were created by the test - const createdFiles = this.filesAfterCompile.filter(v => this.existingFiles.indexOf(v) < 0); - for (const file of createdFiles) { - fs.unlinkSync(file); - } - this.existingFiles = []; - this.filesAfterCompile = []; - } - -} diff --git a/test/compiler/projects/baseurl/test_src/main.ts b/test/compiler/projects/baseurl/test_src/main.ts deleted file mode 100644 index 62afd2ad1..000000000 --- a/test/compiler/projects/baseurl/test_src/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as test from "nested/lib_file"; - -let hello = "12"; - -hello = "13"; diff --git a/test/compiler/projects/baseurl/test_src/test_lib/nested/lib_file.ts b/test/compiler/projects/baseurl/test_src/test_lib/nested/lib_file.ts deleted file mode 100644 index 4248a042b..000000000 --- a/test/compiler/projects/baseurl/test_src/test_lib/nested/lib_file.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function test() { - return 1; -} diff --git a/test/compiler/projects/baseurl/tsconfig.json b/test/compiler/projects/baseurl/tsconfig.json deleted file mode 100644 index 69a82043b..000000000 --- a/test/compiler/projects/baseurl/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "luaTarget": "JIT", - "compilerOptions": { - "outDir": "./out_dir", - "rootDir": ".", - "baseUrl": "./test_src/test_lib", - } -} diff --git a/test/compiler/projects/basic/test_src/test_lib/file.ts b/test/compiler/projects/basic/test_src/test_lib/file.ts deleted file mode 100755 index d764cbd99..000000000 --- a/test/compiler/projects/basic/test_src/test_lib/file.ts +++ /dev/null @@ -1 +0,0 @@ -const test = true; diff --git a/test/compiler/projects/basic/tsconfig.bothDirOptions.json b/test/compiler/projects/basic/tsconfig.bothDirOptions.json deleted file mode 100755 index b21881ae5..000000000 --- a/test/compiler/projects/basic/tsconfig.bothDirOptions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "out_dir", - "rootDir": "test_src" - } -} diff --git a/test/compiler/projects/basic/tsconfig.json b/test/compiler/projects/basic/tsconfig.json deleted file mode 100755 index e5a835e99..000000000 --- a/test/compiler/projects/basic/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "luaTarget": "JIT" -} diff --git a/test/compiler/projects/basic/tsconfig.outDir.json b/test/compiler/projects/basic/tsconfig.outDir.json deleted file mode 100755 index cd2de0d24..000000000 --- a/test/compiler/projects/basic/tsconfig.outDir.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "out_dir" - } -} diff --git a/test/compiler/projects/basic/tsconfig.rootDir.json b/test/compiler/projects/basic/tsconfig.rootDir.json deleted file mode 100755 index d9506e7ee..000000000 --- a/test/compiler/projects/basic/tsconfig.rootDir.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "test_src" - } -} diff --git a/test/compiler/projects/watchmode/tsconfig.json b/test/compiler/projects/watchmode/tsconfig.json deleted file mode 100644 index e5a835e99..000000000 --- a/test/compiler/projects/watchmode/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "luaTarget": "JIT" -} diff --git a/test/compiler/projects/watchmode/watch.ts b/test/compiler/projects/watchmode/watch.ts deleted file mode 100644 index 4e2a15260..000000000 --- a/test/compiler/projects/watchmode/watch.ts +++ /dev/null @@ -1 +0,0 @@ -class MyTest {} \ No newline at end of file diff --git a/test/compiler/testfiles/default_import.ts b/test/compiler/testfiles/default_import.ts deleted file mode 100644 index 9f80d14ac..000000000 --- a/test/compiler/testfiles/default_import.ts +++ /dev/null @@ -1 +0,0 @@ -import Test from "./default_export"; diff --git a/test/compiler/testfiles/invalid_syntax.ts b/test/compiler/testfiles/invalid_syntax.ts deleted file mode 100644 index 6d38d3866..000000000 --- a/test/compiler/testfiles/invalid_syntax.ts +++ /dev/null @@ -1 +0,0 @@ -const variable = () => {} => {}; diff --git a/test/compiler/testfiles/out_file.ts b/test/compiler/testfiles/out_file.ts deleted file mode 100644 index 45842c5e2..000000000 --- a/test/compiler/testfiles/out_file.ts +++ /dev/null @@ -1,3 +0,0 @@ -class Test { - -} diff --git a/test/compiler/testfiles/watch.ts b/test/compiler/testfiles/watch.ts deleted file mode 100644 index 4e2a15260..000000000 --- a/test/compiler/testfiles/watch.ts +++ /dev/null @@ -1 +0,0 @@ -class MyTest {} \ No newline at end of file diff --git a/test/compiler/watcher_proccess.ts b/test/compiler/watcher_proccess.ts deleted file mode 100644 index 25b4bd344..000000000 --- a/test/compiler/watcher_proccess.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { compile } from "../../src/Compiler"; - -process.on("message", args => { - compile(args); -}); diff --git a/test/compiler/watchmode.spec.ts b/test/compiler/watchmode.spec.ts deleted file mode 100644 index 7b838316b..000000000 --- a/test/compiler/watchmode.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { AsyncTest, Expect, Setup, TestCase, Timeout } from "alsatian"; -import { fork } from "child_process"; -import * as fs from "fs"; -import * as path from "path"; - -export class CompilerWatchModeTest { - - @TestCase(["-w", path.join(__dirname, "./testfiles/watch.ts")], - path.join(__dirname, "./testfiles/watch.ts")) - @TestCase(["-w", "-p", path.join(__dirname, "./projects/watchmode/")], - path.join(__dirname, "./projects/watchmode/watch.ts")) - @AsyncTest("Watch single File") - @Timeout(16000) - public async testSingle(args: string[], fileToChange: string): Promise { - fileToChange = fileToChange; - const fileToChangeOut = fileToChange.replace(".ts", ".lua"); - - const child = fork(path.join(__dirname, "watcher_proccess.js")); - child.send(args); - - await this.waitForFileExists(fileToChangeOut, 9000) - .catch(err => console.error(err)); - - Expect(fs.existsSync(fileToChangeOut)).toBe(true); - - const initialResultLua = fs.readFileSync(fileToChangeOut); - const originalTS = fs.readFileSync(fileToChange); - - fs.unlinkSync(fileToChangeOut); - - fs.writeFileSync(fileToChange, "class MyTest2 {}"); - - await this.waitForFileExists(fileToChangeOut, 5000) - .catch(err => console.error(err)); - - const updatedResultLua = fs.readFileSync(fileToChangeOut).toString(); - - Expect(initialResultLua).not.toEqual(updatedResultLua); - - fs.writeFileSync(fileToChange, originalTS); - - fs.unlinkSync(fileToChangeOut); - - child.kill(); - } - - private waitForFileExists(filepath: string, timeout: number = 3000): Promise { - const interval = 200; - return new Promise((resolve, reject) => { - const intervalTimerId = setInterval( - () => { - if (fs.existsSync(filepath)) { - clearTimeout(timeoutId); - clearInterval(intervalTimerId); - resolve(); - } - }, - interval); - - const timeoutId = setTimeout( - () => { - clearInterval(intervalTimerId); - reject(new Error("Wating for file timed out!")); - }, - timeout); - }); - } -} diff --git a/test/json.50.lua b/test/json.50.lua new file mode 100644 index 000000000..8e4109c8f --- /dev/null +++ b/test/json.50.lua @@ -0,0 +1,147 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- +-- +-- LICENSE CONTENTS: +-- +-- Copyright (c) 2015 rxi +-- +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if k ~= "n" then + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + end + if n ~= table.getn(val) then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. string.gsub(val, '[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + if val ~= val then + return "NaN" + elseif val >= 1 / 0 then + return "Infinity" + elseif val <= 0 / 0 then + return "-Infinity" + else + return string.format("%.17g", val) + end +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + +return {stringify = function(val) return ( encode(val) ) end} diff --git a/test/src/json.lua b/test/json.lua similarity index 93% rename from test/src/json.lua rename to test/json.lua index 6dafe9fe4..cd345c9b4 100644 --- a/test/src/json.lua +++ b/test/json.lua @@ -112,11 +112,15 @@ end local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") + if val ~= val then + return "NaN" + elseif val == math.huge then + return "Infinity" + elseif val == -math.huge then + return "-Infinity" + else + return string.format("%.17g", val) end - return string.format("%.14g", val) end @@ -138,7 +142,4 @@ encode = function(val, stack) error("unexpected type '" .. t .. "'") end - -function JSONStringify(val) - return ( encode(val) ) -end +return {stringify = function(val) return ( encode(val) ) end} \ No newline at end of file diff --git a/test/runner.ts b/test/runner.ts deleted file mode 100644 index b2dfbbf2b..000000000 --- a/test/runner.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { TestOutcome, TestRunner, TestSet } from "alsatian"; - -import * as fs from "fs"; -import * as path from "path"; - -// create test set -const testSet = TestSet.create(); - -// add your tests -testSet.addTestsFromFiles("./test/**/*.spec.js"); - -// create a test runner -const testRunner = new TestRunner(); - -// Copy lualib to project root -fs.copyFileSync( - path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), - "lualib_bundle.lua" -); - -// setup the output -testRunner.outputStream - // pipe to the console - .pipe(process.stdout); - -let success = 0; -let ignored = 0; -let run = 0; -testRunner.onTestComplete(test => { - run++; - - if (test.outcome === TestOutcome.Pass) { - success++; - } else if (test.outcome === TestOutcome.Skip) { - ignored++; - } -}); - -// run the test set -testRunner.run(testSet) - // this will be called after all tests have been run - .then(result => { - // Remove lualib bundle again - fs.unlinkSync("lualib_bundle.lua"); - - const nonIgnoredTests = run - ignored; - const failedTests = nonIgnoredTests - success; - console.log(`Ignored ${ignored}/${run} tests.`); - console.log(`Failed ${failedTests}/${nonIgnoredTests} tests.`); - console.log(`Passed ${success}/${nonIgnoredTests} tests.`); - - if (failedTests > 0) { - process.exit(1); - } - }) - // this will be called if there was a problem - .catch(error => { - // Remove lualib bundle again - fs.unlinkSync("lualib_bundle.lua"); - - console.error(error); - process.exit(1); - }); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 000000000..e431d8519 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,48 @@ +import * as assert from "assert"; +import * as ts from "typescript"; +import * as tstl from "../src"; + +declare global { + namespace jest { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Matchers { + toHaveDiagnostics(expected?: number[]): R; + } + } +} + +expect.extend({ + toHaveDiagnostics(diagnostics: ts.Diagnostic[], expected?: number[]): jest.CustomMatcherResult { + assert(Array.isArray(diagnostics)); + // @ts-ignore + const matcherHint = this.utils.matcherHint("toHaveDiagnostics", undefined, "", this); + + const diagnosticMessages = ts.formatDiagnosticsWithColorAndContext( + diagnostics.map(tstl.prepareDiagnosticForFormatting), + { getCurrentDirectory: () => "", getCanonicalFileName: fileName => fileName, getNewLine: () => "\n" } + ); + + if (this.isNot && expected !== undefined) { + throw new Error("expect(actual).not.toHaveDiagnostics(expected) is not supported"); + } + + return { + pass: expected + ? diagnostics.length === expected.length && + diagnostics.every((diag, index) => diag.code === expected[index]) + : diagnostics.length > 0, + + message: () => { + const message = this.isNot + ? diagnosticMessages + : expected + ? `Expected:\n${expected.join("\n")}\nReceived:\n${diagnostics + .map(diag => diag.code) + .join("\n")}\n\n${diagnosticMessages}\n` + : `Received: ${this.utils.printReceived([])}\n`; + + return matcherHint + "\n\n" + message; + }, + }; + }, +}); diff --git a/test/src/util.ts b/test/src/util.ts deleted file mode 100644 index a27bcc851..000000000 --- a/test/src/util.ts +++ /dev/null @@ -1,147 +0,0 @@ -import * as path from "path"; -import * as ts from "typescript"; - -import { Expect } from "alsatian"; - -import { transpileString as compilerTranspileString, createStringCompilerProgram } from "../../src/Compiler"; -import { CompilerOptions, LuaTarget, LuaLibImportKind } from "../../src/CompilerOptions"; - -import {lauxlib, lua, lualib, to_jsstring, to_luastring } from "fengari"; - -import * as fs from "fs"; -import { LuaTransformer } from "../../src/LuaTransformer"; - -export function transpileString( - str: string, - options?: CompilerOptions, - ignoreDiagnostics = true, - filePath = "file.ts"): string { - if (options) { - if (options.noHeader === undefined) { - options.noHeader = true; - } - return compilerTranspileString(str, options, ignoreDiagnostics, filePath); - } else { - return compilerTranspileString( - str, - { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - noHeader: true, - }, - ignoreDiagnostics, - filePath - ); - } -} - -export function executeLua(luaStr: string, withLib = true): any { - if (withLib) { - luaStr = minimalTestLib + luaStr; - } - - const L = lauxlib.luaL_newstate(); - lualib.luaL_openlibs(L); - const status = lauxlib.luaL_dostring(L, to_luastring(luaStr)); - - if (status === lua.LUA_OK) { - // Read the return value from stack depending on its type. - if (lua.lua_isboolean(L, -1)) { - return lua.lua_toboolean(L, -1); - } else if (lua.lua_isnil(L, -1)) { - return undefined; - } else if (lua.lua_isnumber(L, -1)) { - return lua.lua_tonumber(L, -1); - } else if (lua.lua_isstring(L, -1)) { - return lua.lua_tojsstring(L, -1); - } else { - throw new Error("Unsupported lua return type: " + to_jsstring(lua.lua_typename(L, lua.lua_type(L, -1)))); - } - } else { - // If the lua VM did not terminate with status code LUA_OK an error occurred. - // Throw a JS error with the message, retrieved by reading a string from the stack. - - // Filter control characters out of string which are in there because ???? - throw new Error("LUA ERROR: " + to_jsstring(lua.lua_tostring(L, -1).filter(c => c >= 20))); - } -} - -export function expectCodeEqual(code1: string, code2: string): void { - // Trim leading/trailing whitespace - let c1 = code1.trim(); - let c2 = code2.trim(); - - // Unify indentation - c1 = c1.replace(/\s+/g, " "); - c2 = c2.replace(/\s+/g, " "); - - Expect(c1).toBe(c2); -} - -// Get a mock transformer to use for testing -export function makeTestTransformer(target: LuaTarget = LuaTarget.Lua53): LuaTransformer { - const options = {luaTarget: target}; - return new LuaTransformer(ts.createProgram([], options), options); -} - -export function transpileAndExecute( - tsStr: string, - compilerOptions?: CompilerOptions, - luaHeader?: string, - tsHeader?: string, - ignoreDiagnosticsOverride = process.argv[2] === "--ignoreDiagnostics" -): any -{ - const wrappedTsString = `declare function JSONStringify(p: any): string; - ${tsHeader ? tsHeader : ""} - function __runTest(): any {${tsStr}}`; - - const lua = `${luaHeader ? luaHeader : ""} - ${transpileString(wrappedTsString, compilerOptions, ignoreDiagnosticsOverride)} - return __runTest();`; - - return executeLua(lua); -} - -export function transpileExecuteAndReturnExport( - tsStr: string, - returnExport: string, - compilerOptions?: CompilerOptions, - luaHeader?: string -): any -{ - const wrappedTsString = `declare function JSONStringify(p: any): string; - ${tsStr}`; - - const lua = `return (function() - ${luaHeader ? luaHeader : ""} - ${transpileString(wrappedTsString, compilerOptions, false)} - end)().${returnExport}`; - - return executeLua(lua); -} - -export function parseTypeScript(typescript: string, target: LuaTarget = LuaTarget.Lua53) - : [ts.SourceFile, ts.TypeChecker] { - const program = createStringCompilerProgram(typescript, { luaTarget: target }); - return [program.getSourceFile("file.ts"), program.getTypeChecker()]; -} - -export function findFirstChild(node: ts.Node, predicate: (node: ts.Node) => boolean): ts.Node | undefined { - for (const child of node.getChildren()) { - if (predicate(child)) { - return child; - } - - const childChild = findFirstChild(child, predicate); - if (childChild !== undefined) { - return childChild; - } - } - return undefined; -} - -const jsonlib = fs.readFileSync("test/src/json.lua") + "\n"; - -export const minimalTestLib = jsonlib; diff --git a/test/test_thread.ts b/test/test_thread.ts deleted file mode 100644 index d9404d609..000000000 --- a/test/test_thread.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MatchError, TestRunner, TestSet, TestOutcome } from "alsatian"; -import * as flatted from "flatted"; - -module.exports = (input, done) => { - const testSet = TestSet.create(); - testSet.addTestsFromFiles(input.files); - const testRunner = new TestRunner(); - - let testCount = 0; - let failedTestCount = 0; - - testRunner.onTestComplete(result => { - if (result.outcome === TestOutcome.Fail) { - if (result.error instanceof MatchError) { - console.log(`Test ${result.testFixture.description}, ${result.test.key}(${flatted.stringify(result.testCase.caseArguments)}) Failed!`); - console.log(" ---\n" + - ' message: "' + - result.error.message + - '"\n' + - " severity: fail\n" + - " data:\n" + - " got: " + - result.error.actual + - "\n" + - " expect: " + - result.error.expected + - "\n"); - } - failedTestCount++; - } - testCount++; - }); - - testRunner.run(testSet) - .then(() => done(testCount, failedTestCount)); -}; diff --git a/test/threaded_runner.ts b/test/threaded_runner.ts deleted file mode 100644 index ba8f42000..000000000 --- a/test/threaded_runner.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {MatchError} from "alsatian"; -import * as glob from "glob"; -import * as os from "os"; -import * as path from "path"; -import {config, Pool} from "threads"; - -function fileArrToString(fileArr: string[]): string { - return fileArr.map(val => path.basename(val).replace(".spec.js", "")).join(", "); -} - -function printTestStats(testCount: number, failedTestCount: number, header: string, footer: string): void { - console.log("-----------------"); - console.log(header); - console.log(`Total: ${testCount}`); - console.log(`Passed: ${testCount - failedTestCount}`); - console.log(`Failed: ${failedTestCount}`); - console.log(footer); - console.log("-----------------"); -} - -config.set({ - basepath: { - node: __dirname, - } -}); - -let cpuCount = os.cpus().length + 1; -if ("TRAVIS" in process.env && "CI" in process.env) { - // fixed thread count for CI - cpuCount = 8; -} -const testFiles: string[] = glob.sync("./test/**/*.spec.js"); -const pool = new Pool(cpuCount); -let jobCounter = 0; -const testStartTime = new Date(); -const fileCount = testFiles.length; -let exitWithError = false; -let totalTestCount = 0; -let totalFailedTestCount = 0; - -console.log( - `Running tests: ${fileArrToString(testFiles)} with ${cpuCount} threads`); - -testFiles.forEach(file => { - pool.run("./test_thread") - .send({files: [file]}) - .on("done", - (testCount, failedTestCount) => { - if (failedTestCount !== 0) { - exitWithError = true; - } - totalTestCount += testCount; - totalFailedTestCount += failedTestCount; - jobCounter++; - printTestStats( - testCount, - failedTestCount, - `Tests ${file} results:`, - `Thread: ${jobCounter}/${fileCount} done.`); - }) - .on("error", error => { - console.log("Fatal non test related Exception in test file:", file, error); - }); -}); - -pool.on("finished", () => { - let footer = "All tests passed!"; - if (exitWithError) { - footer = "Exiting with Error: One or more tests failed!"; - } - printTestStats(totalTestCount, totalFailedTestCount, "Final Results:", footer); - - console.log("Everything done, shutting down the thread pool."); - const timeInMs = (new Date().valueOf() - testStartTime.valueOf()); - console.log(`Tests took: ${Math.floor(timeInMs / 1000 / 60)}:${Math.floor(timeInMs / 1000) % 60}`); - - pool.killAll(); - - if (exitWithError) { - process.exit(1); - } -}); - diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap new file mode 100644 index 000000000..e9fccb4ae --- /dev/null +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -0,0 +1,306 @@ +// 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 + local b = 1 + local ____temp_0 = {c = 1} + local c = ____temp_0.c +end" +`; + +exports[`Transformation (characterEscapeSequence) 1`] = ` +"quoteInDoubleQuotes = "' ' '" +quoteInTemplateString = "' ' '" +doubleQuoteInQuotes = "\\" \\" \\"" +doubleQuoteInDoubleQuotes = "\\" \\" \\"" +doubleQuoteInTemplateString = "\\" \\" \\"" +backQuoteInQuotes = "\` \` \`" +backQuoteInDoubleQuotes = "\` \` \`" +backQuoteInTemplateString = "\` \` \`" +escapedCharsInQuotes = "\\\\ \\0 \\b \\t \\n \\v \\f \\" ' \`" +escapedCharsInDoubleQuotes = "\\\\ \\0 \\b \\t \\n \\v \\f \\" ' \`" +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`] = ` +"local ____exports = {} +local xyz = 4 +____exports.xyz = xyz +____exports.uwv = xyz +do + local ____export = require("xyz") + for ____exportKey, ____exportValue in pairs(____export) do + if ____exportKey ~= "default" then + ____exports[____exportKey] = ____exportValue + end + end +end +do + local ____xyz = require("xyz") + ____exports.abc = ____xyz.abc + ____exports.def = ____xyz.def +end +do + local ____xyz = require("xyz") + ____exports.def = ____xyz.abc +end +do + local ____bla = require("bla") + ____exports.bar = ____bla["123"] +end +do + local ____bla = require("bla") + ____exports["123"] = ____bla.foo +end +return ____exports" +`; + +exports[`Transformation (globalAugmentation) 1`] = ` +"local ____exports = globalVariable +return ____exports" +`; + +exports[`Transformation (methodRestArguments) 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +MyClass = __TS__Class() +MyClass.name = "MyClass" +function MyClass.prototype.____constructor(self) +end +function MyClass.prototype.varargsFunction(self, a, ...) +end" +`; + +exports[`Transformation (modulesChangedVariableExport) 1`] = ` +"local ____exports = {} +____exports.foo = 1 +return ____exports" +`; + +exports[`Transformation (modulesClassExport) 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local ____exports = {} +____exports.TestClass = __TS__Class() +local TestClass = ____exports.TestClass +TestClass.name = "TestClass" +function TestClass.prototype.____constructor(self) +end +return ____exports" +`; + +exports[`Transformation (modulesClassWithMemberExport) 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local ____exports = {} +____exports.TestClass = __TS__Class() +local TestClass = ____exports.TestClass +TestClass.name = "TestClass" +function TestClass.prototype.____constructor(self) +end +function TestClass.prototype.memberFunc(self) +end +return ____exports" +`; + +exports[`Transformation (modulesFunctionExport) 1`] = ` +"local ____exports = {} +function ____exports.publicFunc(self) +end +return ____exports" +`; + +exports[`Transformation (modulesFunctionNoExport) 1`] = ` +"function publicFunc(self) +end" +`; + +exports[`Transformation (modulesImportAll) 1`] = ` +"local ____exports = {} +local Test = require("test") +local ____ = Test +return ____exports" +`; + +exports[`Transformation (modulesImportNamed) 1`] = ` +"local ____exports = {} +local ____test = require("test") +local TestClass = ____test.TestClass +local ____ = TestClass +return ____exports" +`; + +exports[`Transformation (modulesImportNamedSpecialChars) 1`] = ` +"local ____exports = {} +local ____kebab_2Dmodule = require("kebab-module") +local TestClass1 = ____kebab_2Dmodule.TestClass1 +local ____dollar_24module = require("dollar$module") +local TestClass2 = ____dollar_24module.TestClass2 +local ____singlequote_27module = require("singlequote'module") +local TestClass3 = ____singlequote_27module.TestClass3 +local ____hash_23module = require("hash#module") +local TestClass4 = ____hash_23module.TestClass4 +local ____space_20module = require("space module") +local TestClass5 = ____space_20module.TestClass5 +local ____ = TestClass1 +local ____ = TestClass2 +local ____ = TestClass3 +local ____ = TestClass4 +local ____ = TestClass5 +return ____exports" +`; + +exports[`Transformation (modulesImportRenamed) 1`] = ` +"local ____exports = {} +local ____test = require("test") +local RenamedClass = ____test.TestClass +local ____ = RenamedClass +return ____exports" +`; + +exports[`Transformation (modulesImportRenamedSpecialChars) 1`] = ` +"local ____exports = {} +local ____kebab_2Dmodule = require("kebab-module") +local RenamedClass1 = ____kebab_2Dmodule.TestClass +local ____dollar_24module = require("dollar$module") +local RenamedClass2 = ____dollar_24module.TestClass +local ____singlequote_27module = require("singlequote'module") +local RenamedClass3 = ____singlequote_27module.TestClass +local ____hash_23module = require("hash#module") +local RenamedClass4 = ____hash_23module.TestClass +local ____space_20module = require("space module") +local RenamedClass5 = ____space_20module.TestClass +local ____ = RenamedClass1 +local ____ = RenamedClass2 +local ____ = RenamedClass3 +local ____ = RenamedClass4 +local ____ = RenamedClass5 +return ____exports" +`; + +exports[`Transformation (modulesImportWithoutFromClause) 1`] = ` +"local ____exports = {} +require("test") +return ____exports" +`; + +exports[`Transformation (modulesNamespaceExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +return ____exports" +`; + +exports[`Transformation (modulesNamespaceNestedWithMemberExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +local TestSpace = ____exports.TestSpace +do + TestSpace.TestNestedSpace = {} + local TestNestedSpace = TestSpace.TestNestedSpace + do + function TestNestedSpace.innerFunc(self) + end + end +end +return ____exports" +`; + +exports[`Transformation (modulesNamespaceNoExport) 1`] = `"TestSpace = TestSpace or ({})"`; + +exports[`Transformation (modulesNamespaceWithMemberExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +local TestSpace = ____exports.TestSpace +do + function TestSpace.innerFunc(self) + end +end +return ____exports" +`; + +exports[`Transformation (modulesNamespaceWithMemberNoExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +do + local function innerFunc(self) + end +end +return ____exports" +`; + +exports[`Transformation (modulesVariableExport) 1`] = ` +"local ____exports = {} +____exports.foo = "bar" +return ____exports" +`; + +exports[`Transformation (modulesVariableNoExport) 1`] = `"foo = "bar""`; + +exports[`Transformation (printFormat) 1`] = ` +"stringConcat = (("a" .. "b" .. "c") .. "d") .. "e" +numbers = 2 * 2 + 3 + 4 * (5 + 6) ~= 7 +function func(...) +end +func(function() + local b = "A function" +end) +func(func()) +array = {func()} +array2 = { + func(), + func() +} +object = {a = 1, b = 2, c = 3} +bigObject = { + a = 1, + b = 2, + c = 3, + d = "value1", + e = "value2" +}" +`; + +exports[`Transformation (returnDefault) 1`] = ` +"function myFunc(self) + return +end" +`; + +exports[`Transformation (topLevelVariables) 1`] = ` +"obj = {value1 = 1, value2 = 2} +value1 = obj.value1 +value2 = obj.value2 +obj2 = {value3 = 1, value4 = 2} +value3 = obj2.value3 +local ____obj2_0 = obj2 +value4 = ____obj2_0.value4 +function fun1(self) +end +fun2 = function() +end" +`; + +exports[`Transformation (unusedDefaultWithNamespaceImport) 1`] = ` +"local ____exports = {} +local x = require("module") +local ____ = x +return ____exports" +`; diff --git a/test/translation/builder.spec.ts b/test/translation/builder.spec.ts deleted file mode 100644 index f667a6fd0..000000000 --- a/test/translation/builder.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Expect, Test, TestCases } from "alsatian"; - -import * as util from "../src/util"; - -import * as fs from "fs"; -import * as path from "path"; - -const files: string[][] = []; -const fileContents: {[key: string]: Buffer} = {}; - -const tsPath = path.join(__dirname, "./ts/"); -const luaPath = path.join(__dirname, "./lua/"); - -const tsFiles = fs.readdirSync(tsPath); -const luaFiles = fs.readdirSync(luaPath); - -tsFiles.forEach( - (tsFile, i) => { - // ignore non ts files - if (path.extname(tsFile) !== ".ts") { - return; - } - const luaPart = luaFiles.indexOf(tsFile.replace(".ts", ".lua")); - if (luaPart === -1) { - throw new Error("Missing lua counter part for test file: " + tsFile); - } - const luaFile = luaFiles[luaPart]; - const luaFileAbsolute = path.join(luaPath, luaFile); - const tsFileAbsolute = path.join(tsPath, tsFile); - files.push([tsFile, luaFile]); - fileContents[tsFile] = fs.readFileSync(tsFileAbsolute); - fileContents[luaFile] = fs.readFileSync(luaFileAbsolute); - } -); - -function BufferToTestString(b: Buffer): string { - return b.toString().trim().split("\r\n").join("\n"); -} - -export class FileTests { - - @TestCases(files) - @Test("Transformation Tests") - public transformationTests(tsFile: string, luaFile: string) { - Expect(util.transpileString(BufferToTestString(fileContents[tsFile]))) - .toEqual(BufferToTestString(fileContents[luaFile])); - } - -} diff --git a/test/translation/lua/callNamespace.lua b/test/translation/lua/callNamespace.lua deleted file mode 100644 index b0ec0d650..000000000 --- a/test/translation/lua/callNamespace.lua +++ /dev/null @@ -1 +0,0 @@ -Namespace.myFunction(); diff --git a/test/translation/lua/characterEscapeSequence.lua b/test/translation/lua/characterEscapeSequence.lua deleted file mode 100644 index 36abfe9d0..000000000 --- a/test/translation/lua/characterEscapeSequence.lua +++ /dev/null @@ -1,9 +0,0 @@ -local quoteInDoubleQuotes = "\' \' \'"; -local quoteInTemplateString = "\' \' \'"; -local doubleQuoteInQuotes = "\" \" \""; -local doubleQuoteInDoubleQuotes = "\" \" \""; -local doubleQuoteInTemplateString = "\" \" \""; -local escapedCharsInQuotes = "\\ \0 \b \t \n \v \f \" \' \`"; -local escapedCharsInDoubleQUotes = "\\ \0 \b \t \n \v \f \" \' \`"; -local escapedCharsInTemplateString = "\\ \0 \b \t \n \v \f \" \' \`"; -local nonEmptyTemplateString = "Level 0: \n\t " .. (tostring("Level 1: \n\t\t " .. (tostring("Level 3: \n\t\t\t " .. (tostring("Last level \n --") .. " \n --")) .. " \n --")) .. " \n --"); diff --git a/test/translation/lua/classExtension1.lua b/test/translation/lua/classExtension1.lua deleted file mode 100644 index ff9e5bb5d..000000000 --- a/test/translation/lua/classExtension1.lua +++ /dev/null @@ -1,2 +0,0 @@ -MyClass.myFunction = function(self) -end; diff --git a/test/translation/lua/classExtension2.lua b/test/translation/lua/classExtension2.lua deleted file mode 100644 index 94e8f655f..000000000 --- a/test/translation/lua/classExtension2.lua +++ /dev/null @@ -1,2 +0,0 @@ -TestClass.myFunction = function(self) -end; diff --git a/test/translation/lua/classExtension3.lua b/test/translation/lua/classExtension3.lua deleted file mode 100644 index 008d45956..000000000 --- a/test/translation/lua/classExtension3.lua +++ /dev/null @@ -1,4 +0,0 @@ -RenamedTestClass.myFunction = function(self) -end; -RenamedMyClass.myFunction = function(self) -end; diff --git a/test/translation/lua/classExtension4.lua b/test/translation/lua/classExtension4.lua deleted file mode 100644 index 8fcb73124..000000000 --- a/test/translation/lua/classExtension4.lua +++ /dev/null @@ -1,4 +0,0 @@ -MyClass.test = "test"; -MyClass.testP = "testP"; -MyClass.myFunction = function(self) -end; diff --git a/test/translation/lua/classPureAbstract.lua b/test/translation/lua/classPureAbstract.lua deleted file mode 100644 index 70ba4efb3..000000000 --- a/test/translation/lua/classPureAbstract.lua +++ /dev/null @@ -1,12 +0,0 @@ -ClassB = ClassB or {}; -ClassB.__index = ClassB; -ClassB.prototype = ClassB.prototype or {}; -ClassB.prototype.__index = ClassB.prototype; -ClassB.prototype.constructor = ClassB; -ClassB.new = function(...) - local self = setmetatable({}, ClassB.prototype); - self:____constructor(...); - return self; -end; -ClassB.prototype.____constructor = function(self) -end; diff --git a/test/translation/lua/continue.lua b/test/translation/lua/continue.lua deleted file mode 100644 index 25ccd79f1..000000000 --- a/test/translation/lua/continue.lua +++ /dev/null @@ -1,12 +0,0 @@ -do - local i = 0; - while i < 10 do - do - if i < 5 then - goto __continue1; - end - end - ::__continue1:: - i = i + 1; - end -end diff --git a/test/translation/lua/continueConcurrent.lua b/test/translation/lua/continueConcurrent.lua deleted file mode 100644 index 6dbd5c31c..000000000 --- a/test/translation/lua/continueConcurrent.lua +++ /dev/null @@ -1,15 +0,0 @@ -do - local i = 0; - while i < 10 do - do - if i < 5 then - goto __continue1; - end - if i == 7 then - goto __continue1; - end - end - ::__continue1:: - i = i + 1; - end -end diff --git a/test/translation/lua/continueNested.lua b/test/translation/lua/continueNested.lua deleted file mode 100644 index 18da6fbf7..000000000 --- a/test/translation/lua/continueNested.lua +++ /dev/null @@ -1,24 +0,0 @@ -do - local i = 0; - while i < 5 do - do - if (i % 2) == 0 then - goto __continue1; - end - do - local j = 0; - while j < 2 do - do - if j == 1 then - goto __continue3; - end - end - ::__continue3:: - j = j + 1; - end - end - end - ::__continue1:: - i = i + 1; - end -end diff --git a/test/translation/lua/continueNestedConcurrent.lua b/test/translation/lua/continueNestedConcurrent.lua deleted file mode 100644 index 55a1b7f2a..000000000 --- a/test/translation/lua/continueNestedConcurrent.lua +++ /dev/null @@ -1,27 +0,0 @@ -do - local i = 0; - while i < 5 do - do - if (i % 2) == 0 then - goto __continue1; - end - do - local j = 0; - while j < 2 do - do - if j == 1 then - goto __continue3; - end - end - ::__continue3:: - j = j + 1; - end - end - if i == 4 then - goto __continue1; - end - end - ::__continue1:: - i = i + 1; - end -end diff --git a/test/translation/lua/do.lua b/test/translation/lua/do.lua deleted file mode 100644 index 14639278d..000000000 --- a/test/translation/lua/do.lua +++ /dev/null @@ -1,7 +0,0 @@ -local e = 10; -repeat - do - e = e - 1; - end - ::__continue1:: -until not (e > 0); diff --git a/test/translation/lua/enum.lua b/test/translation/lua/enum.lua deleted file mode 100644 index ab48266d7..000000000 --- a/test/translation/lua/enum.lua +++ /dev/null @@ -1,7 +0,0 @@ -TestEnum = {}; -TestEnum.val1 = 0; -TestEnum[0] = "val1"; -TestEnum.val2 = 2; -TestEnum[2] = "val2"; -TestEnum.val3 = 3; -TestEnum[3] = "val3"; diff --git a/test/translation/lua/enumHeterogeneous.lua b/test/translation/lua/enumHeterogeneous.lua deleted file mode 100644 index fec3ab147..000000000 --- a/test/translation/lua/enumHeterogeneous.lua +++ /dev/null @@ -1,7 +0,0 @@ -TestEnum = {}; -TestEnum.val1 = 0; -TestEnum[0] = "val1"; -TestEnum.val2 = 3; -TestEnum[3] = "val2"; -TestEnum.val3 = "baz"; -TestEnum.baz = "val3"; diff --git a/test/translation/lua/enumMembersOnly.lua b/test/translation/lua/enumMembersOnly.lua deleted file mode 100644 index 06bdb5f70..000000000 --- a/test/translation/lua/enumMembersOnly.lua +++ /dev/null @@ -1,5 +0,0 @@ -val1 = 0; -val2 = 2; -val3 = 3; -val4 = "bye"; -local a = val1; diff --git a/test/translation/lua/enumString.lua b/test/translation/lua/enumString.lua deleted file mode 100644 index f35f146a6..000000000 --- a/test/translation/lua/enumString.lua +++ /dev/null @@ -1,7 +0,0 @@ -TestEnum = {}; -TestEnum.val1 = "foo"; -TestEnum.foo = "val1"; -TestEnum.val2 = "bar"; -TestEnum.bar = "val2"; -TestEnum.val3 = "baz"; -TestEnum.baz = "val3"; diff --git a/test/translation/lua/exportStatement.lua b/test/translation/lua/exportStatement.lua deleted file mode 100644 index 2369b004b..000000000 --- a/test/translation/lua/exportStatement.lua +++ /dev/null @@ -1,23 +0,0 @@ -local exports = exports or {}; -local xyz = 4; -exports.xyz = xyz; -exports.uwv = xyz; -do - local __TSTL_export = require("xyz"); - for ____exportKey, ____exportValue in pairs(__TSTL_export) do - exports[____exportKey] = ____exportValue; - end -end -do - local __TSTL_xyz = require("xyz"); - local abc = __TSTL_xyz.abc; - local def = __TSTL_xyz.def; - exports.abc = abc; - exports.def = def; -end -do - local __TSTL_xyz = require("xyz"); - local def = __TSTL_xyz.abc; - exports.def = def; -end -return exports; \ No newline at end of file diff --git a/test/translation/lua/for.lua b/test/translation/lua/for.lua deleted file mode 100644 index 9f0dfb360..000000000 --- a/test/translation/lua/for.lua +++ /dev/null @@ -1,9 +0,0 @@ -do - local i = 1; - while i <= 100 do - do - end - ::__continue1:: - i = i + 1; - end -end diff --git a/test/translation/lua/forIn.lua b/test/translation/lua/forIn.lua deleted file mode 100644 index 5882e705d..000000000 --- a/test/translation/lua/forIn.lua +++ /dev/null @@ -1,5 +0,0 @@ -for i in pairs({a = 1, b = 2, c = 3, d = 4}) do - do - end - ::__continue1:: -end diff --git a/test/translation/lua/forOf.lua b/test/translation/lua/forOf.lua deleted file mode 100644 index ce8676017..000000000 --- a/test/translation/lua/forOf.lua +++ /dev/null @@ -1,7 +0,0 @@ -local ____TS_array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; -for ____TS_index = 1, #____TS_array do - local i = ____TS_array[____TS_index]; - do - end - ::__continue1:: -end diff --git a/test/translation/lua/functionRestArguments.lua b/test/translation/lua/functionRestArguments.lua deleted file mode 100644 index 25fe06c01..000000000 --- a/test/translation/lua/functionRestArguments.lua +++ /dev/null @@ -1,3 +0,0 @@ -varargsFunction = function(a, ...) - local b = ({...}); -end; diff --git a/test/translation/lua/getSetAccessors.lua b/test/translation/lua/getSetAccessors.lua deleted file mode 100644 index a4188e9c7..000000000 --- a/test/translation/lua/getSetAccessors.lua +++ /dev/null @@ -1,26 +0,0 @@ -require("lualib_bundle"); -MyClass = MyClass or {}; -MyClass.__index = MyClass; -MyClass.prototype = MyClass.prototype or {}; -MyClass.prototype.____getters = {}; -MyClass.prototype.__index = __TS__Index(MyClass.prototype); -MyClass.prototype.____setters = {}; -MyClass.prototype.__newindex = __TS__NewIndex(MyClass.prototype); -MyClass.prototype.constructor = MyClass; -MyClass.new = function(...) - local self = setmetatable({}, MyClass.prototype); - self:____constructor(...); - return self; -end; -MyClass.prototype.____constructor = function(self) -end; -MyClass.prototype.____getters.field = function(self) - return self._field + 4; -end; -MyClass.prototype.____setters.field = function(self, v) - self._field = v * 2; -end; -local instance = MyClass.new(); -instance.field = 4; -local b = instance.field; -local c = (4 + instance.field) * 3; diff --git a/test/translation/lua/interfaceIndex.lua b/test/translation/lua/interfaceIndex.lua deleted file mode 100644 index a29d6d09d..000000000 --- a/test/translation/lua/interfaceIndex.lua +++ /dev/null @@ -1,2 +0,0 @@ -local a = {}; -a.abc = "def"; diff --git a/test/translation/lua/methodRestArguments.lua b/test/translation/lua/methodRestArguments.lua deleted file mode 100644 index 0f2ba1555..000000000 --- a/test/translation/lua/methodRestArguments.lua +++ /dev/null @@ -1,15 +0,0 @@ -MyClass = MyClass or {}; -MyClass.__index = MyClass; -MyClass.prototype = MyClass.prototype or {}; -MyClass.prototype.__index = MyClass.prototype; -MyClass.prototype.constructor = MyClass; -MyClass.new = function(...) - local self = setmetatable({}, MyClass.prototype); - self:____constructor(...); - return self; -end; -MyClass.prototype.____constructor = function(self) -end; -MyClass.prototype.varargsFunction = function(self, a, ...) - local b = ({...}); -end; diff --git a/test/translation/lua/modulesChangedVariableExport.lua b/test/translation/lua/modulesChangedVariableExport.lua deleted file mode 100644 index aa02323b6..000000000 --- a/test/translation/lua/modulesChangedVariableExport.lua +++ /dev/null @@ -1,3 +0,0 @@ -local exports = exports or {}; -exports.test = 1; -return exports; diff --git a/test/translation/lua/modulesClassExport.lua b/test/translation/lua/modulesClassExport.lua deleted file mode 100644 index 48dd160d5..000000000 --- a/test/translation/lua/modulesClassExport.lua +++ /dev/null @@ -1,14 +0,0 @@ -local exports = exports or {}; -exports.TestClass = exports.TestClass or {}; -exports.TestClass.__index = exports.TestClass; -exports.TestClass.prototype = exports.TestClass.prototype or {}; -exports.TestClass.prototype.__index = exports.TestClass.prototype; -exports.TestClass.prototype.constructor = exports.TestClass; -exports.TestClass.new = function(...) - local self = setmetatable({}, exports.TestClass.prototype); - self:____constructor(...); - return self; -end; -exports.TestClass.prototype.____constructor = function(self) -end; -return exports; diff --git a/test/translation/lua/modulesClassWithMemberExport.lua b/test/translation/lua/modulesClassWithMemberExport.lua deleted file mode 100644 index f4f63ab0b..000000000 --- a/test/translation/lua/modulesClassWithMemberExport.lua +++ /dev/null @@ -1,16 +0,0 @@ -local exports = exports or {}; -exports.TestClass = exports.TestClass or {}; -exports.TestClass.__index = exports.TestClass; -exports.TestClass.prototype = exports.TestClass.prototype or {}; -exports.TestClass.prototype.__index = exports.TestClass.prototype; -exports.TestClass.prototype.constructor = exports.TestClass; -exports.TestClass.new = function(...) - local self = setmetatable({}, exports.TestClass.prototype); - self:____constructor(...); - return self; -end; -exports.TestClass.prototype.____constructor = function(self) -end; -exports.TestClass.prototype.memberFunc = function(self) -end; -return exports; diff --git a/test/translation/lua/modulesFunctionExport.lua b/test/translation/lua/modulesFunctionExport.lua deleted file mode 100644 index 12e69553c..000000000 --- a/test/translation/lua/modulesFunctionExport.lua +++ /dev/null @@ -1,4 +0,0 @@ -local exports = exports or {}; -exports.publicFunc = function() -end; -return exports; diff --git a/test/translation/lua/modulesFunctionNoExport.lua b/test/translation/lua/modulesFunctionNoExport.lua deleted file mode 100644 index 37ea15ea9..000000000 --- a/test/translation/lua/modulesFunctionNoExport.lua +++ /dev/null @@ -1,2 +0,0 @@ -publicFunc = function() -end; diff --git a/test/translation/lua/modulesImportAll.lua b/test/translation/lua/modulesImportAll.lua deleted file mode 100644 index 334fc07a7..000000000 --- a/test/translation/lua/modulesImportAll.lua +++ /dev/null @@ -1 +0,0 @@ -local Test = require("test"); diff --git a/test/translation/lua/modulesImportNamed.lua b/test/translation/lua/modulesImportNamed.lua deleted file mode 100644 index 27cbae8c9..000000000 --- a/test/translation/lua/modulesImportNamed.lua +++ /dev/null @@ -1,2 +0,0 @@ -local __TSTL_test = require("test"); -local TestClass = __TSTL_test.TestClass; diff --git a/test/translation/lua/modulesImportNamedSpecialChars.lua b/test/translation/lua/modulesImportNamedSpecialChars.lua deleted file mode 100644 index 837c6ae21..000000000 --- a/test/translation/lua/modulesImportNamedSpecialChars.lua +++ /dev/null @@ -1,10 +0,0 @@ -local __TSTL_kebab_module = require("kebab-module"); -local TestClass = __TSTL_kebab_module.TestClass; -local __TSTL_dollar_module = require("dollar$module"); -local TestClass = __TSTL_dollar_module.TestClass; -local __TSTL_singlequote_module = require("singlequote'module"); -local TestClass = __TSTL_singlequote_module.TestClass; -local __TSTL_hash_module = require("hash#module"); -local TestClass = __TSTL_hash_module.TestClass; -local __TSTL_space_module = require("space module"); -local TestClass = __TSTL_space_module.TestClass; diff --git a/test/translation/lua/modulesImportRenamed.lua b/test/translation/lua/modulesImportRenamed.lua deleted file mode 100644 index 84c484b5d..000000000 --- a/test/translation/lua/modulesImportRenamed.lua +++ /dev/null @@ -1,2 +0,0 @@ -local __TSTL_test = require("test"); -local RenamedClass = __TSTL_test.TestClass; diff --git a/test/translation/lua/modulesImportRenamedSpecialChars.lua b/test/translation/lua/modulesImportRenamedSpecialChars.lua deleted file mode 100644 index 000113c65..000000000 --- a/test/translation/lua/modulesImportRenamedSpecialChars.lua +++ /dev/null @@ -1,10 +0,0 @@ -local __TSTL_kebab_module = require("kebab-module"); -local RenamedClass = __TSTL_kebab_module.TestClass; -local __TSTL_dollar_module = require("dollar$module"); -local RenamedClass = __TSTL_dollar_module.TestClass; -local __TSTL_singlequote_module = require("singlequote'module"); -local RenamedClass = __TSTL_singlequote_module.TestClass; -local __TSTL_hash_module = require("hash#module"); -local RenamedClass = __TSTL_hash_module.TestClass; -local __TSTL_space_module = require("space module"); -local RenamedClass = __TSTL_space_module.TestClass; diff --git a/test/translation/lua/modulesImportWithoutFromClause.lua b/test/translation/lua/modulesImportWithoutFromClause.lua deleted file mode 100644 index e7b7d56c8..000000000 --- a/test/translation/lua/modulesImportWithoutFromClause.lua +++ /dev/null @@ -1 +0,0 @@ -require("test"); diff --git a/test/translation/lua/modulesNamespaceExport.lua b/test/translation/lua/modulesNamespaceExport.lua deleted file mode 100644 index 122f64cf0..000000000 --- a/test/translation/lua/modulesNamespaceExport.lua +++ /dev/null @@ -1,6 +0,0 @@ -local exports = exports or {}; -exports.TestSpace = exports.TestSpace or {}; -local TestSpace = exports.TestSpace; -do -end -return exports; diff --git a/test/translation/lua/modulesNamespaceExportEnum.lua b/test/translation/lua/modulesNamespaceExportEnum.lua deleted file mode 100644 index 5e3997f00..000000000 --- a/test/translation/lua/modulesNamespaceExportEnum.lua +++ /dev/null @@ -1,11 +0,0 @@ -local exports = exports or {}; -exports.test = exports.test or {}; -local test = exports.test; -do - test.TestEnum = {}; - test.TestEnum.foo = "foo"; - test.TestEnum.foo = "foo"; - test.TestEnum.bar = "bar"; - test.TestEnum.bar = "bar"; -end -return exports; diff --git a/test/translation/lua/modulesNamespaceNestedWithMemberExport.lua b/test/translation/lua/modulesNamespaceNestedWithMemberExport.lua deleted file mode 100644 index 44509f50d..000000000 --- a/test/translation/lua/modulesNamespaceNestedWithMemberExport.lua +++ /dev/null @@ -1,12 +0,0 @@ -local exports = exports or {}; -exports.TestSpace = exports.TestSpace or {}; -local TestSpace = exports.TestSpace; -do - TestSpace.TestNestedSpace = TestSpace.TestNestedSpace or {}; - local TestNestedSpace = TestSpace.TestNestedSpace; - do - TestNestedSpace.innerFunc = function() - end; - end -end -return exports; diff --git a/test/translation/lua/modulesNamespaceNoExport.lua b/test/translation/lua/modulesNamespaceNoExport.lua deleted file mode 100644 index 9f4892365..000000000 --- a/test/translation/lua/modulesNamespaceNoExport.lua +++ /dev/null @@ -1,3 +0,0 @@ -TestSpace = TestSpace or {}; -do -end diff --git a/test/translation/lua/modulesNamespaceWithMemberExport.lua b/test/translation/lua/modulesNamespaceWithMemberExport.lua deleted file mode 100644 index 020bcf4d5..000000000 --- a/test/translation/lua/modulesNamespaceWithMemberExport.lua +++ /dev/null @@ -1,8 +0,0 @@ -local exports = exports or {}; -exports.TestSpace = exports.TestSpace or {}; -local TestSpace = exports.TestSpace; -do - TestSpace.innerFunc = function() - end; -end -return exports; diff --git a/test/translation/lua/modulesNamespaceWithMemberNoExport.lua b/test/translation/lua/modulesNamespaceWithMemberNoExport.lua deleted file mode 100644 index b0abeb433..000000000 --- a/test/translation/lua/modulesNamespaceWithMemberNoExport.lua +++ /dev/null @@ -1,9 +0,0 @@ -local exports = exports or {}; -exports.TestSpace = exports.TestSpace or {}; -local TestSpace = exports.TestSpace; -do - local innerFunc; - innerFunc = function() - end; -end -return exports; diff --git a/test/translation/lua/modulesVariableExport.lua b/test/translation/lua/modulesVariableExport.lua deleted file mode 100644 index 76c98810f..000000000 --- a/test/translation/lua/modulesVariableExport.lua +++ /dev/null @@ -1,3 +0,0 @@ -local exports = exports or {}; -exports.test = "test"; -return exports; diff --git a/test/translation/lua/modulesVariableNoExport.lua b/test/translation/lua/modulesVariableNoExport.lua deleted file mode 100644 index f20c4efeb..000000000 --- a/test/translation/lua/modulesVariableNoExport.lua +++ /dev/null @@ -1 +0,0 @@ -local test = "test"; diff --git a/test/translation/lua/namespace.lua b/test/translation/lua/namespace.lua deleted file mode 100644 index bdf6664fd..000000000 --- a/test/translation/lua/namespace.lua +++ /dev/null @@ -1,6 +0,0 @@ -myNamespace = myNamespace or {}; -do - local nsMember; - nsMember = function() - end; -end diff --git a/test/translation/lua/namespaceMerge.lua b/test/translation/lua/namespaceMerge.lua deleted file mode 100644 index 3c515447c..000000000 --- a/test/translation/lua/namespaceMerge.lua +++ /dev/null @@ -1,35 +0,0 @@ -MergedClass = MergedClass or {}; -MergedClass.__index = MergedClass; -MergedClass.prototype = MergedClass.prototype or {}; -MergedClass.prototype.__index = MergedClass.prototype; -MergedClass.prototype.constructor = MergedClass; -MergedClass.new = function(...) - local self = setmetatable({}, MergedClass.prototype); - self:____constructor(...); - return self; -end; -MergedClass.prototype.____constructor = function(self) - self.propertyFunc = function(____) - end; -end; -MergedClass.staticMethodA = function(self) -end; -MergedClass.staticMethodB = function(self) - self:staticMethodA(); -end; -MergedClass.prototype.methodA = function(self) -end; -MergedClass.prototype.methodB = function(self) - self:methodA(); - self:propertyFunc(); -end; -MergedClass = MergedClass or {}; -do - MergedClass.namespaceFunc = function() - end; -end -local mergedClass = MergedClass.new(); -mergedClass:methodB(); -mergedClass:propertyFunc(); -MergedClass:staticMethodB(); -MergedClass.namespaceFunc(); diff --git a/test/translation/lua/namespaceNested.lua b/test/translation/lua/namespaceNested.lua deleted file mode 100644 index dd1006ee9..000000000 --- a/test/translation/lua/namespaceNested.lua +++ /dev/null @@ -1,10 +0,0 @@ -myNamespace = myNamespace or {}; -do - myNamespace.myNestedNamespace = myNamespace.myNestedNamespace or {}; - local myNestedNamespace = myNamespace.myNestedNamespace; - do - local nsMember; - nsMember = function() - end; - end -end diff --git a/test/translation/lua/namespacePhantom.lua b/test/translation/lua/namespacePhantom.lua deleted file mode 100644 index 3844b51be..000000000 --- a/test/translation/lua/namespacePhantom.lua +++ /dev/null @@ -1,2 +0,0 @@ -nsMember = function() -end; diff --git a/test/translation/lua/returnDefault.lua b/test/translation/lua/returnDefault.lua deleted file mode 100644 index a0e35c570..000000000 --- a/test/translation/lua/returnDefault.lua +++ /dev/null @@ -1,3 +0,0 @@ -myFunc = function() - return; -end; diff --git a/test/translation/lua/shorthandPropertyAssignment.lua b/test/translation/lua/shorthandPropertyAssignment.lua deleted file mode 100644 index 0c5898188..000000000 --- a/test/translation/lua/shorthandPropertyAssignment.lua +++ /dev/null @@ -1,4 +0,0 @@ -local f; -f = function(x) - return ({x = x}); -end; diff --git a/test/translation/lua/tryCatch.lua b/test/translation/lua/tryCatch.lua deleted file mode 100644 index f3194765e..000000000 --- a/test/translation/lua/tryCatch.lua +++ /dev/null @@ -1,8 +0,0 @@ -do - local ____TS_try, er = pcall(function() - local a = 42; - end); - if not ____TS_try then - local b = "fail"; - end -end diff --git a/test/translation/lua/tryCatchFinally.lua b/test/translation/lua/tryCatchFinally.lua deleted file mode 100644 index 4b5caf9a6..000000000 --- a/test/translation/lua/tryCatchFinally.lua +++ /dev/null @@ -1,11 +0,0 @@ -do - local ____TS_try, er = pcall(function() - local a = 42; - end); - if not ____TS_try then - local b = "fail"; - end - do - local c = "finally"; - end -end diff --git a/test/translation/lua/tryFinally.lua b/test/translation/lua/tryFinally.lua deleted file mode 100644 index a117b8bb2..000000000 --- a/test/translation/lua/tryFinally.lua +++ /dev/null @@ -1,8 +0,0 @@ -do - pcall(function() - local a = 42; - end); - do - local b = "finally"; - end -end diff --git a/test/translation/lua/tupleReturn.lua b/test/translation/lua/tupleReturn.lua deleted file mode 100644 index c8d492bec..000000000 --- a/test/translation/lua/tupleReturn.lua +++ /dev/null @@ -1,28 +0,0 @@ -tupleReturn = function() - return 0, "foobar"; -end; -tupleReturn(); -noTupleReturn(); -local a, b = tupleReturn(); -local c, d = table.unpack(noTupleReturn()); -a, b = tupleReturn(); -c, d = table.unpack(noTupleReturn()); -local e = ({tupleReturn()}); -local f = noTupleReturn(); -e = ({tupleReturn()}); -f = noTupleReturn(); -foo(({tupleReturn()})); -foo(noTupleReturn()); -tupleReturnFromVar = function() - local r = {1, "baz"}; - return table.unpack(r); -end; -tupleReturnForward = function() - return tupleReturn(); -end; -tupleNoForward = function() - return ({tupleReturn()}); -end; -tupleReturnUnpack = function() - return table.unpack(tupleNoForward()); -end; diff --git a/test/translation/lua/typeAssert.lua b/test/translation/lua/typeAssert.lua deleted file mode 100644 index 1c7069d36..000000000 --- a/test/translation/lua/typeAssert.lua +++ /dev/null @@ -1,2 +0,0 @@ -local test1 = 10; -local test2 = 10; diff --git a/test/translation/lua/while.lua b/test/translation/lua/while.lua deleted file mode 100644 index a779e9ded..000000000 --- a/test/translation/lua/while.lua +++ /dev/null @@ -1,7 +0,0 @@ -local d = 10; -while d > 0 do - do - d = d - 1; - end - ::__continue1:: -end diff --git a/test/translation/transformation.spec.ts b/test/translation/transformation.spec.ts new file mode 100644 index 000000000..5f9df58fe --- /dev/null +++ b/test/translation/transformation.spec.ts @@ -0,0 +1,36 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as tstl from "../../src"; +import { annotationDeprecated } from "../../src/transformation/utils/diagnostics"; +import { couldNotResolveRequire } from "../../src/transpilation/diagnostics"; +import * as util from "../util"; + +const fixturesPath = path.join(__dirname, "./transformation"); +const fixtures = fs + .readdirSync(fixturesPath) + .filter(f => path.extname(f) === ".ts") + .sort() + .map(f => [path.parse(f).name, fs.readFileSync(path.join(fixturesPath, f), "utf8")]); + +test.each(fixtures)("Transformation (%s)", (_name, content) => { + util.testModule(content) + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require }) + .ignoreDiagnostics([annotationDeprecated.code, couldNotResolveRequire.code]) + .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/blockScopeVariables.ts b/test/translation/transformation/blockScopeVariables.ts new file mode 100644 index 000000000..7623f0efa --- /dev/null +++ b/test/translation/transformation/blockScopeVariables.ts @@ -0,0 +1,5 @@ +{ + const a = 1; + const [b] = [1]; + const { c } = { c: 1 }; +} diff --git a/test/translation/ts/characterEscapeSequence.ts b/test/translation/transformation/characterEscapeSequence.ts similarity index 61% rename from test/translation/ts/characterEscapeSequence.ts rename to test/translation/transformation/characterEscapeSequence.ts index f99683c32..eebf8f293 100644 --- a/test/translation/ts/characterEscapeSequence.ts +++ b/test/translation/transformation/characterEscapeSequence.ts @@ -5,8 +5,12 @@ let doubleQuoteInQuotes = '" " "'; let doubleQuoteInDoubleQuotes = "\" \" \""; let doubleQuoteInTemplateString = `" " "`; +let backQuoteInQuotes = "` ` `"; +let backQuoteInDoubleQuotes = "` ` `"; +let backQuoteInTemplateString = `\` \` \``; + let escapedCharsInQuotes = '\\ \0 \b \t \n \v \f \" \' \`'; -let escapedCharsInDoubleQUotes = "\\ \0 \b \t \n \v \f \" \' \`"; +let escapedCharsInDoubleQuotes = "\\ \0 \b \t \n \v \f \" \' \`"; let escapedCharsInTemplateString = `\\ \0 \b \t \n \v \f \" \' \``; -let nonEmptyTemplateString = `Level 0: \n\t ${`Level 1: \n\t\t ${`Level 3: \n\t\t\t ${'Last level \n --'} \n --`} \n --`} \n --`; +let nonEmptyTemplateString = `Level 0: \n\t ${`Level 1: \n\t\t ${`Level 3: \n\t\t\t ${"Last level \n --"} \n --`} \n --`} \n --`; 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/customNameWithNoSelf.ts b/test/translation/transformation/customNameWithNoSelf.ts new file mode 100644 index 000000000..73175ccdc --- /dev/null +++ b/test/translation/transformation/customNameWithNoSelf.ts @@ -0,0 +1,7 @@ +/** @noSelf */ +declare namespace TestNamespace { + /** @customName pass */ + function fail(): void; +} + +TestNamespace.fail(); diff --git a/test/translation/transformation/exportStatement.ts b/test/translation/transformation/exportStatement.ts new file mode 100644 index 000000000..cf227da3d --- /dev/null +++ b/test/translation/transformation/exportStatement.ts @@ -0,0 +1,8 @@ +const xyz = 4; +export { xyz }; +export { xyz as uwv }; +export * from "xyz"; +export { abc, def } from "xyz"; +export { abc as def } from "xyz"; +export { "123" as bar } from "bla"; +export { foo as "123" } from "bla"; diff --git a/test/translation/transformation/globalAugmentation.ts b/test/translation/transformation/globalAugmentation.ts new file mode 100644 index 000000000..03f2c5af2 --- /dev/null +++ b/test/translation/transformation/globalAugmentation.ts @@ -0,0 +1,5 @@ +declare global { + export const globalVariable: number; +} + +export = globalVariable; 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/translation/ts/methodRestArguments.ts b/test/translation/transformation/methodRestArguments.ts similarity index 100% rename from test/translation/ts/methodRestArguments.ts rename to test/translation/transformation/methodRestArguments.ts diff --git a/test/translation/transformation/modulesChangedVariableExport.ts b/test/translation/transformation/modulesChangedVariableExport.ts new file mode 100644 index 000000000..009c4cd06 --- /dev/null +++ b/test/translation/transformation/modulesChangedVariableExport.ts @@ -0,0 +1,2 @@ +export let foo; +foo = 1; diff --git a/test/translation/ts/modulesClassExport.ts b/test/translation/transformation/modulesClassExport.ts similarity index 100% rename from test/translation/ts/modulesClassExport.ts rename to test/translation/transformation/modulesClassExport.ts diff --git a/test/translation/ts/modulesClassWithMemberExport.ts b/test/translation/transformation/modulesClassWithMemberExport.ts similarity index 100% rename from test/translation/ts/modulesClassWithMemberExport.ts rename to test/translation/transformation/modulesClassWithMemberExport.ts diff --git a/test/translation/ts/modulesFunctionExport.ts b/test/translation/transformation/modulesFunctionExport.ts similarity index 100% rename from test/translation/ts/modulesFunctionExport.ts rename to test/translation/transformation/modulesFunctionExport.ts diff --git a/test/translation/ts/modulesFunctionNoExport.ts b/test/translation/transformation/modulesFunctionNoExport.ts similarity index 100% rename from test/translation/ts/modulesFunctionNoExport.ts rename to test/translation/transformation/modulesFunctionNoExport.ts diff --git a/test/translation/transformation/modulesImportAll.ts b/test/translation/transformation/modulesImportAll.ts new file mode 100644 index 000000000..28b470640 --- /dev/null +++ b/test/translation/transformation/modulesImportAll.ts @@ -0,0 +1,3 @@ +import * as Test from "test"; + +Test; diff --git a/test/translation/transformation/modulesImportNamed.ts b/test/translation/transformation/modulesImportNamed.ts new file mode 100644 index 000000000..d147081ff --- /dev/null +++ b/test/translation/transformation/modulesImportNamed.ts @@ -0,0 +1,3 @@ +import { TestClass } from "test"; + +TestClass; diff --git a/test/translation/transformation/modulesImportNamedSpecialChars.ts b/test/translation/transformation/modulesImportNamedSpecialChars.ts new file mode 100644 index 000000000..dbb54990c --- /dev/null +++ b/test/translation/transformation/modulesImportNamedSpecialChars.ts @@ -0,0 +1,11 @@ +import { TestClass1 } from "kebab-module"; +import { TestClass2 } from "dollar$module"; +import { TestClass3 } from "singlequote'module"; +import { TestClass4 } from "hash#module"; +import { TestClass5 } from "space module"; + +TestClass1; +TestClass2; +TestClass3; +TestClass4; +TestClass5; diff --git a/test/translation/transformation/modulesImportRenamed.ts b/test/translation/transformation/modulesImportRenamed.ts new file mode 100644 index 000000000..74c82598b --- /dev/null +++ b/test/translation/transformation/modulesImportRenamed.ts @@ -0,0 +1,3 @@ +import { TestClass as RenamedClass } from "test"; + +RenamedClass; diff --git a/test/translation/transformation/modulesImportRenamedSpecialChars.ts b/test/translation/transformation/modulesImportRenamedSpecialChars.ts new file mode 100644 index 000000000..a200c07e9 --- /dev/null +++ b/test/translation/transformation/modulesImportRenamedSpecialChars.ts @@ -0,0 +1,11 @@ +import { TestClass as RenamedClass1 } from "kebab-module"; +import { TestClass as RenamedClass2 } from "dollar$module"; +import { TestClass as RenamedClass3 } from "singlequote'module"; +import { TestClass as RenamedClass4 } from "hash#module"; +import { TestClass as RenamedClass5 } from "space module"; + +RenamedClass1; +RenamedClass2; +RenamedClass3; +RenamedClass4; +RenamedClass5; diff --git a/test/translation/ts/modulesImportWithoutFromClause.ts b/test/translation/transformation/modulesImportWithoutFromClause.ts similarity index 100% rename from test/translation/ts/modulesImportWithoutFromClause.ts rename to test/translation/transformation/modulesImportWithoutFromClause.ts diff --git a/test/translation/ts/modulesNamespaceExport.ts b/test/translation/transformation/modulesNamespaceExport.ts similarity index 100% rename from test/translation/ts/modulesNamespaceExport.ts rename to test/translation/transformation/modulesNamespaceExport.ts diff --git a/test/translation/ts/modulesNamespaceNestedWithMemberExport.ts b/test/translation/transformation/modulesNamespaceNestedWithMemberExport.ts similarity index 100% rename from test/translation/ts/modulesNamespaceNestedWithMemberExport.ts rename to test/translation/transformation/modulesNamespaceNestedWithMemberExport.ts diff --git a/test/translation/ts/modulesNamespaceNoExport.ts b/test/translation/transformation/modulesNamespaceNoExport.ts similarity index 100% rename from test/translation/ts/modulesNamespaceNoExport.ts rename to test/translation/transformation/modulesNamespaceNoExport.ts diff --git a/test/translation/ts/modulesNamespaceWithMemberExport.ts b/test/translation/transformation/modulesNamespaceWithMemberExport.ts similarity index 100% rename from test/translation/ts/modulesNamespaceWithMemberExport.ts rename to test/translation/transformation/modulesNamespaceWithMemberExport.ts diff --git a/test/translation/ts/modulesNamespaceWithMemberNoExport.ts b/test/translation/transformation/modulesNamespaceWithMemberNoExport.ts similarity index 100% rename from test/translation/ts/modulesNamespaceWithMemberNoExport.ts rename to test/translation/transformation/modulesNamespaceWithMemberNoExport.ts diff --git a/test/translation/transformation/modulesVariableExport.ts b/test/translation/transformation/modulesVariableExport.ts new file mode 100644 index 000000000..d407b0602 --- /dev/null +++ b/test/translation/transformation/modulesVariableExport.ts @@ -0,0 +1 @@ +export const foo = "bar"; diff --git a/test/translation/transformation/modulesVariableNoExport.ts b/test/translation/transformation/modulesVariableNoExport.ts new file mode 100644 index 000000000..4f4b4c843 --- /dev/null +++ b/test/translation/transformation/modulesVariableNoExport.ts @@ -0,0 +1 @@ +const foo = "bar"; diff --git a/test/translation/transformation/printFormat.ts b/test/translation/transformation/printFormat.ts new file mode 100644 index 000000000..3374952c5 --- /dev/null +++ b/test/translation/transformation/printFormat.ts @@ -0,0 +1,27 @@ +const stringConcat = "a" + ("b" + "c") + "d" + "e"; +const numbers = 2 * 2 + 3 + 4 * (5 + 6) !== 7; + +function func(this: void, ...args: any) {} + +func(() => { + const b = "A function"; +}); + +func(func()); + +const array = [func()]; +const array2 = [func(), func()]; + +const object = { + a: 1, + b: 2, + c: 3, +}; + +const bigObject = { + a: 1, + b: 2, + c: 3, + d: "value1", + e: "value2", +}; diff --git a/test/translation/ts/returnDefault.ts b/test/translation/transformation/returnDefault.ts similarity index 100% rename from test/translation/ts/returnDefault.ts rename to test/translation/transformation/returnDefault.ts diff --git a/test/translation/transformation/topLevelVariables.ts b/test/translation/transformation/topLevelVariables.ts new file mode 100644 index 000000000..5782c4cf4 --- /dev/null +++ b/test/translation/transformation/topLevelVariables.ts @@ -0,0 +1,11 @@ +const obj = { value1: 1, value2: 2 }; +const value1 = obj.value1; +const { value2 } = obj; + +let noValueLet; +let obj2 = { value3: 1, value4: 2 }; +let value3 = obj2.value3; +let { value4 } = obj2; + +function fun1(): void {} +const fun2 = () => {}; diff --git a/test/translation/transformation/unusedDefaultWithNamespaceImport.ts b/test/translation/transformation/unusedDefaultWithNamespaceImport.ts new file mode 100644 index 000000000..5b66a4b22 --- /dev/null +++ b/test/translation/transformation/unusedDefaultWithNamespaceImport.ts @@ -0,0 +1,2 @@ +import def, * as x from "module"; +x; diff --git a/test/translation/ts/callNamespace.ts b/test/translation/ts/callNamespace.ts deleted file mode 100644 index e87b7b751..000000000 --- a/test/translation/ts/callNamespace.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace Namespace { - function myFunction(); -} -Namespace.myFunction(); \ No newline at end of file diff --git a/test/translation/ts/classExtension1.ts b/test/translation/ts/classExtension1.ts deleted file mode 100644 index 5df5bdeac..000000000 --- a/test/translation/ts/classExtension1.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** @extension */ -class MyClass { - public myFunction() {} -} diff --git a/test/translation/ts/classExtension2.ts b/test/translation/ts/classExtension2.ts deleted file mode 100644 index 158f16422..000000000 --- a/test/translation/ts/classExtension2.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** @extension */ -class TestClass { -} - -/** @extension */ -class MyClass extends TestClass { - public myFunction() {} -} diff --git a/test/translation/ts/classExtension3.ts b/test/translation/ts/classExtension3.ts deleted file mode 100644 index 3fa37f05e..000000000 --- a/test/translation/ts/classExtension3.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** @extension RenamedTestClass */ -class TestClass { - public myFunction() {} -} - -/** @extension RenamedMyClass */ -class MyClass extends TestClass { - public myFunction() {} -} diff --git a/test/translation/ts/classExtension4.ts b/test/translation/ts/classExtension4.ts deleted file mode 100644 index 8cb996079..000000000 --- a/test/translation/ts/classExtension4.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** @extension */ -class MyClass { - public test: string = "test"; - private testP: string = "testP"; - public myFunction() {} -} diff --git a/test/translation/ts/classPureAbstract.ts b/test/translation/ts/classPureAbstract.ts deleted file mode 100644 index 178c23b59..000000000 --- a/test/translation/ts/classPureAbstract.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** @pureAbstract */ -declare class ClassA {} -class ClassB extends ClassA {} \ No newline at end of file diff --git a/test/translation/ts/continue.ts b/test/translation/ts/continue.ts deleted file mode 100644 index 64830718d..000000000 --- a/test/translation/ts/continue.ts +++ /dev/null @@ -1,5 +0,0 @@ -for (let i = 0; i < 10; i++) { - if (i < 5) { - continue; - } -} diff --git a/test/translation/ts/continueConcurrent.ts b/test/translation/ts/continueConcurrent.ts deleted file mode 100644 index ef1b20c68..000000000 --- a/test/translation/ts/continueConcurrent.ts +++ /dev/null @@ -1,9 +0,0 @@ -for (let i = 0; i < 10; i++) { - if (i < 5) { - continue; - } - - if (i === 7) { - continue; - } -} diff --git a/test/translation/ts/continueNested.ts b/test/translation/ts/continueNested.ts deleted file mode 100644 index 563b7d0f1..000000000 --- a/test/translation/ts/continueNested.ts +++ /dev/null @@ -1,11 +0,0 @@ -for (let i = 0; i < 5; i++) { - if (i % 2 === 0) { - continue; - } - - for (let j = 0; j < 2; j++) { - if (j === 1) { - continue; - } - } -} diff --git a/test/translation/ts/continueNestedConcurrent.ts b/test/translation/ts/continueNestedConcurrent.ts deleted file mode 100644 index fe0bd0966..000000000 --- a/test/translation/ts/continueNestedConcurrent.ts +++ /dev/null @@ -1,15 +0,0 @@ -for (let i = 0; i < 5; i++) { - if (i % 2 === 0) { - continue; - } - - for (let j = 0; j < 2; j++) { - if (j === 1) { - continue; - } - } - - if (i === 4) { - continue; - } -} diff --git a/test/translation/ts/do.ts b/test/translation/ts/do.ts deleted file mode 100644 index 62b1c0a65..000000000 --- a/test/translation/ts/do.ts +++ /dev/null @@ -1,4 +0,0 @@ -let e = 10 -do { - e--; -} while (e > 0) diff --git a/test/translation/ts/enum.ts b/test/translation/ts/enum.ts deleted file mode 100644 index a59f7ac76..000000000 --- a/test/translation/ts/enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -enum TestEnum { - val1 = 0, - val2 = 2, - val3 -} \ No newline at end of file diff --git a/test/translation/ts/enumHeterogeneous.ts b/test/translation/ts/enumHeterogeneous.ts deleted file mode 100644 index a047d66fe..000000000 --- a/test/translation/ts/enumHeterogeneous.ts +++ /dev/null @@ -1,5 +0,0 @@ -enum TestEnum { - val1, - val2 = 3, - val3 = "baz", -} diff --git a/test/translation/ts/enumMembersOnly.ts b/test/translation/ts/enumMembersOnly.ts deleted file mode 100644 index 9d384dc6c..000000000 --- a/test/translation/ts/enumMembersOnly.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** @compileMembersOnly */ -enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", -} - -const a = TestEnum.val1; diff --git a/test/translation/ts/enumString.ts b/test/translation/ts/enumString.ts deleted file mode 100644 index 8ab7e30d1..000000000 --- a/test/translation/ts/enumString.ts +++ /dev/null @@ -1,5 +0,0 @@ -enum TestEnum { - val1 = "foo", - val2 = "bar", - val3 = "baz", -} diff --git a/test/translation/ts/exportStatement.ts b/test/translation/ts/exportStatement.ts deleted file mode 100644 index 524be0bd9..000000000 --- a/test/translation/ts/exportStatement.ts +++ /dev/null @@ -1,6 +0,0 @@ -const xyz = 4; -export {xyz}; -export {xyz as uwv}; -export * from "xyz"; -export {abc, def} from "xyz"; -export {abc as def} from "xyz"; diff --git a/test/translation/ts/for.ts b/test/translation/ts/for.ts deleted file mode 100644 index 02d8156e2..000000000 --- a/test/translation/ts/for.ts +++ /dev/null @@ -1,2 +0,0 @@ -for (let i = 1; i <= 100; i++) { -} diff --git a/test/translation/ts/forIn.ts b/test/translation/ts/forIn.ts deleted file mode 100644 index 55e50cce7..000000000 --- a/test/translation/ts/forIn.ts +++ /dev/null @@ -1,7 +0,0 @@ -for (let i in { - a: 1, - b: 2, - c: 3, - d: 4 -}) { -} diff --git a/test/translation/ts/forOf.ts b/test/translation/ts/forOf.ts deleted file mode 100644 index c017d5c0c..000000000 --- a/test/translation/ts/forOf.ts +++ /dev/null @@ -1,2 +0,0 @@ -for (let i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { -} diff --git a/test/translation/ts/functionRestArguments.ts b/test/translation/ts/functionRestArguments.ts deleted file mode 100644 index 3b015a4d6..000000000 --- a/test/translation/ts/functionRestArguments.ts +++ /dev/null @@ -1 +0,0 @@ -function varargsFunction(a: string, ...b: string[]): void {} diff --git a/test/translation/ts/getSetAccessors.ts b/test/translation/ts/getSetAccessors.ts deleted file mode 100644 index b721f0f0b..000000000 --- a/test/translation/ts/getSetAccessors.ts +++ /dev/null @@ -1,14 +0,0 @@ -class MyClass { - private _field: number; - public get field(): number { - return this._field + 4; - } - public set field(v: number) { - this._field = v*2; - } -} - -let instance = new MyClass(); -instance.field = 4; -const b = instance.field; -const c = (4 + instance.field)*3; diff --git a/test/translation/ts/interfaceIndex.ts b/test/translation/ts/interfaceIndex.ts deleted file mode 100644 index f209e153e..000000000 --- a/test/translation/ts/interfaceIndex.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare interface Dictionary { - [index: string]: T; -} - -let a: Dictionary = {}; -a["abc"] = "def"; diff --git a/test/translation/ts/modulesChangedVariableExport.ts b/test/translation/ts/modulesChangedVariableExport.ts deleted file mode 100644 index 6386d1b78..000000000 --- a/test/translation/ts/modulesChangedVariableExport.ts +++ /dev/null @@ -1,2 +0,0 @@ -export let test; -test = 1; diff --git a/test/translation/ts/modulesImportAll.ts b/test/translation/ts/modulesImportAll.ts deleted file mode 100644 index 1ffb29e95..000000000 --- a/test/translation/ts/modulesImportAll.ts +++ /dev/null @@ -1 +0,0 @@ -import * as Test from "test" diff --git a/test/translation/ts/modulesImportNamed.ts b/test/translation/ts/modulesImportNamed.ts deleted file mode 100644 index 14ba1ec8d..000000000 --- a/test/translation/ts/modulesImportNamed.ts +++ /dev/null @@ -1 +0,0 @@ -import {TestClass} from "test" diff --git a/test/translation/ts/modulesImportNamedSpecialChars.ts b/test/translation/ts/modulesImportNamedSpecialChars.ts deleted file mode 100644 index cff06e7c0..000000000 --- a/test/translation/ts/modulesImportNamedSpecialChars.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {TestClass} from "kebab-module" -import {TestClass} from "dollar$module" -import {TestClass} from "singlequote'module" -import {TestClass} from "hash#module" -import {TestClass} from "space module" diff --git a/test/translation/ts/modulesImportRenamed.ts b/test/translation/ts/modulesImportRenamed.ts deleted file mode 100644 index bc19b6b8b..000000000 --- a/test/translation/ts/modulesImportRenamed.ts +++ /dev/null @@ -1 +0,0 @@ -import {TestClass as RenamedClass} from "test" diff --git a/test/translation/ts/modulesImportRenamedSpecialChars.ts b/test/translation/ts/modulesImportRenamedSpecialChars.ts deleted file mode 100644 index 10cc4b0cc..000000000 --- a/test/translation/ts/modulesImportRenamedSpecialChars.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {TestClass as RenamedClass} from "kebab-module" -import {TestClass as RenamedClass} from "dollar$module" -import {TestClass as RenamedClass} from "singlequote'module" -import {TestClass as RenamedClass} from "hash#module" -import {TestClass as RenamedClass} from "space module" diff --git a/test/translation/ts/modulesNamespaceExportEnum.ts b/test/translation/ts/modulesNamespaceExportEnum.ts deleted file mode 100644 index dd40d81cb..000000000 --- a/test/translation/ts/modulesNamespaceExportEnum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export namespace test { - export enum TestEnum { - foo = "foo", - bar = "bar", - } -} diff --git a/test/translation/ts/modulesVariableExport.ts b/test/translation/ts/modulesVariableExport.ts deleted file mode 100644 index 13f4d5b45..000000000 --- a/test/translation/ts/modulesVariableExport.ts +++ /dev/null @@ -1 +0,0 @@ -export const test = "test" diff --git a/test/translation/ts/modulesVariableNoExport.ts b/test/translation/ts/modulesVariableNoExport.ts deleted file mode 100644 index 858f3dd7c..000000000 --- a/test/translation/ts/modulesVariableNoExport.ts +++ /dev/null @@ -1 +0,0 @@ -const test = "test" diff --git a/test/translation/ts/namespace.ts b/test/translation/ts/namespace.ts deleted file mode 100644 index d9ae73274..000000000 --- a/test/translation/ts/namespace.ts +++ /dev/null @@ -1,3 +0,0 @@ -namespace myNamespace { - function nsMember() {} -} \ No newline at end of file diff --git a/test/translation/ts/namespaceMerge.ts b/test/translation/ts/namespaceMerge.ts deleted file mode 100644 index b6905def9..000000000 --- a/test/translation/ts/namespaceMerge.ts +++ /dev/null @@ -1,24 +0,0 @@ -class MergedClass { - public static staticMethodA(): void {} - public static staticMethodB(): void { - this.staticMethodA(); - } - - public propertyFunc: () => void = () => {}; - - public methodA(): void {} - public methodB(): void { - this.methodA(); - this.propertyFunc(); - } -} - -namespace MergedClass { - export function namespaceFunc(): void {} -} - -const mergedClass = new MergedClass(); -mergedClass.methodB(); -mergedClass.propertyFunc(); -MergedClass.staticMethodB(); -MergedClass.namespaceFunc(); diff --git a/test/translation/ts/namespaceNested.ts b/test/translation/ts/namespaceNested.ts deleted file mode 100644 index 85869547f..000000000 --- a/test/translation/ts/namespaceNested.ts +++ /dev/null @@ -1,5 +0,0 @@ -namespace myNamespace { - namespace myNestedNamespace { - function nsMember() {} - } -} diff --git a/test/translation/ts/namespacePhantom.ts b/test/translation/ts/namespacePhantom.ts deleted file mode 100644 index c99b88e8d..000000000 --- a/test/translation/ts/namespacePhantom.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** @phantom */ -namespace myNamespace { - function nsMember() {} -} \ No newline at end of file diff --git a/test/translation/ts/shorthandPropertyAssignment.ts b/test/translation/ts/shorthandPropertyAssignment.ts deleted file mode 100644 index 4b2aa7e82..000000000 --- a/test/translation/ts/shorthandPropertyAssignment.ts +++ /dev/null @@ -1 +0,0 @@ -const f = x => ({ x }); diff --git a/test/translation/ts/tryCatch.ts b/test/translation/ts/tryCatch.ts deleted file mode 100644 index d5e2f1310..000000000 --- a/test/translation/ts/tryCatch.ts +++ /dev/null @@ -1,5 +0,0 @@ -try { - let a = 42; -} catch (er) { - let b = "fail"; -} diff --git a/test/translation/ts/tryCatchFinally.ts b/test/translation/ts/tryCatchFinally.ts deleted file mode 100644 index dfbd57c7f..000000000 --- a/test/translation/ts/tryCatchFinally.ts +++ /dev/null @@ -1,7 +0,0 @@ -try { - let a = 42; -} catch (er) { - let b = "fail"; -} finally { - let c = "finally"; -} diff --git a/test/translation/ts/tryFinally.ts b/test/translation/ts/tryFinally.ts deleted file mode 100644 index d33f4b95a..000000000 --- a/test/translation/ts/tryFinally.ts +++ /dev/null @@ -1,5 +0,0 @@ -try { - let a = 42; -} finally { - let b = "finally"; -} diff --git a/test/translation/ts/tupleReturn.ts b/test/translation/ts/tupleReturn.ts deleted file mode 100644 index 0d2936071..000000000 --- a/test/translation/ts/tupleReturn.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** @tupleReturn */ -function tupleReturn(): [number, string] { - return [0, "foobar"]; -} -declare function noTupleReturn(): [number, string]; -declare function foo(a: [number, string]): void; -tupleReturn(); -noTupleReturn(); -let [a, b] = tupleReturn(); -let [c, d] = noTupleReturn(); -[a, b] = tupleReturn(); -[c, d] = noTupleReturn(); -let e = tupleReturn(); -let f = noTupleReturn(); -e = tupleReturn(); -f = noTupleReturn(); -foo(tupleReturn()); -foo(noTupleReturn()); -/** @tupleReturn */ -function tupleReturnFromVar(): [number, string] { - const r: [number, string] = [1, "baz"]; - return r; -} -/** @tupleReturn */ -function tupleReturnForward(): [number, string] { - return tupleReturn(); -} -function tupleNoForward(): [number, string] { - return tupleReturn(); -} -/** @tupleReturn */ -function tupleReturnUnpack(): [number, string] { - return tupleNoForward(); -} diff --git a/test/translation/ts/typeAssert.ts b/test/translation/ts/typeAssert.ts deleted file mode 100644 index 95c751a36..000000000 --- a/test/translation/ts/typeAssert.ts +++ /dev/null @@ -1,2 +0,0 @@ -const test1 = 10; -const test2 = 10 as number; diff --git a/test/translation/ts/while.ts b/test/translation/ts/while.ts deleted file mode 100644 index 6101bd4f2..000000000 --- a/test/translation/ts/while.ts +++ /dev/null @@ -1,4 +0,0 @@ -let d = 10; -while (d > 0) { - d--; -} diff --git a/test/transpile/__snapshots__/directories.spec.ts.snap b/test/transpile/__snapshots__/directories.spec.ts.snap new file mode 100644 index 000000000..d214dbba7 --- /dev/null +++ b/test/transpile/__snapshots__/directories.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 1`] = ` +[ + "directories/basic/src/lib/file.lua", + "directories/basic/src/main.lua", +] +`; + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 2`] = ` +[ + "directories/basic/out/lib/file.lua", + "directories/basic/out/main.lua", +] +`; + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 3`] = ` +[ + "directories/basic/src/lib/file.lua", + "directories/basic/src/main.lua", +] +`; + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 4`] = ` +[ + "directories/basic/out/lib/file.lua", + "directories/basic/out/main.lua", +] +`; diff --git a/test/transpile/__snapshots__/module-resolution.spec.ts.snap b/test/transpile/__snapshots__/module-resolution.spec.ts.snap new file mode 100644 index 000000000..c9f0be298 --- /dev/null +++ b/test/transpile/__snapshots__/module-resolution.spec.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`supports complicated paths configuration 1`] = ` +[ + "/paths-base-tsconfig/dist/packages/tstl-program/packages/mypackage/src/bar.lua", + "/paths-base-tsconfig/dist/packages/tstl-program/packages/mypackage/src/index.lua", + "/paths-base-tsconfig/dist/packages/tstl-program/packages/myprogram/src/main.lua", +] +`; + +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", +] +`; diff --git a/test/transpile/__snapshots__/project.spec.ts.snap b/test/transpile/__snapshots__/project.spec.ts.snap new file mode 100644 index 000000000..227e004f8 --- /dev/null +++ b/test/transpile/__snapshots__/project.spec.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should give verbose output 1`] = ` +[ + "Loaded 0 plugins", + "Parsing project settings", + "Transforming /test/transpile/project/otherFile.ts", + "Printing /test/transpile/project/otherFile.ts", + "Transforming /test/transpile/project/index.ts", + "Printing /test/transpile/project/index.ts", + "Constructing emit plan", + "Resolving dependencies for /test/transpile/project/otherFile.ts", + "Resolving dependencies for /test/transpile/project/index.ts", + "Resolving "./otherFile" from /test/transpile/project/index.ts", + "Resolved ./otherFile to /test/transpile/project/otherFile.ts", + "Emitting output", + "Emitting /test/transpile/project/otherFile.lua", + "Emitting /test/transpile/project/index.lua", + "Emit finished!", +] +`; + +exports[`should transpile 1`] = ` +[ + { + "filePath": "otherFile.lua", + "lua": "local ____exports = {} +function ____exports.getNumber(self) + return getAPIValue() +end +return ____exports +", + }, + { + "filePath": "index.lua", + "lua": "local ____exports = {} +local ____otherFile = require("otherFile") +local getNumber = ____otherFile.getNumber +local myNumber = getNumber(nil) +setAPIValue(myNumber * 5) +return ____exports +", + }, +] +`; diff --git a/test/transpile/bundle.spec.ts b/test/transpile/bundle.spec.ts new file mode 100644 index 000000000..3834bbb2f --- /dev/null +++ b/test/transpile/bundle.spec.ts @@ -0,0 +1,132 @@ +import * as path from "path"; +import * as util from "../util"; +import { TranspileVirtualProjectResult } from "../../src"; +import { lineAndColumnOf } from "../unit/printer/utils"; +import * as fs from "fs"; + +describe("bundle two files", () => { + const projectDir = path.join(__dirname, "bundle", "bundle-two-files"); + const inputProject = path.join(projectDir, "tsconfig.json"); + + let transpileResult: TranspileVirtualProjectResult = { + transpiledFiles: [], + diagnostics: [], + }; + + beforeAll(() => { + transpileResult = util.testProject(inputProject).getLuaResult(); + }); + + test("should transpile into one file (with no errors)", () => { + expect(transpileResult.diagnostics).not.toHaveDiagnostics(); + expect(transpileResult.transpiledFiles).toHaveLength(1); + }); + + // Verify the name is as specified in tsconfig + test("should have name, specified in tsconfig.json", () => { + const { outPath } = transpileResult.transpiledFiles[0]; + expect(outPath.endsWith(path.join(projectDir, "bundle.lua"))).toBe(true); + }); + + // Verify exported module by executing + // Use an empty TS string because we already transpiled the TS project + test("executing should act correctly", () => { + const { lua } = transpileResult.transpiledFiles[0]; + util.testModule("").setLuaHeader(lua!).expectToEqual({ myNumber: 3 }); + }); +}); + +describe("bundle with source maps", () => { + const projectDir = path.join(__dirname, "bundle", "bundle-source-maps"); + const inputProject = path.join(projectDir, "tsconfig.json"); + + let transpileResult: TranspileVirtualProjectResult = { + transpiledFiles: [], + diagnostics: [], + }; + + beforeAll(() => { + transpileResult = util.testProject(inputProject).getLuaResult(); + }); + + // See https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1109 + test('the result file should not contain "{#SourceMapTraceback}" macro-string', () => { + const { lua } = transpileResult.transpiledFiles[0]; + expect(lua).toBeDefined(); + expect(lua!).not.toContain("{#SourceMapTraceback}"); + }); + + // Verify exported module by executing + // Use an empty TS string because we already transpiled the TS project + test("executing should act correctly", () => { + const { lua } = transpileResult.transpiledFiles[0]; + const result = util.testModule("").setLuaHeader(lua!).getLuaExecutionResult(); + + expect(result.myNumber).toEqual(3 * 4 * (5 + 6)); + }); + + test("sourceMapTraceback saves correct sourcemap", () => { + const code = { + index: fs.readFileSync(path.join(projectDir, "index.ts"), "utf8"), + largeFile: fs.readFileSync(path.join(projectDir, "largeFile.ts"), "utf8"), + }; + + const { lua } = transpileResult.transpiledFiles[0]; + const builder = util.testModule("").setLuaHeader(lua!); + const result = builder.getLuaExecutionResult(); + const sourceMap = result.sourceMap; + + expect(sourceMap).toEqual(expect.any(Object)); + const sourceMapFiles = Object.keys(sourceMap); + expect(sourceMapFiles).toHaveLength(1); + const mainSourceMap = sourceMap[sourceMapFiles[0]]; + + const transpiledLua = builder.getMainLuaCodeChunk(); + + const assertPatterns: Array<{ + file: keyof typeof code; + luaPattern: string; + typeScriptPattern: string; + }> = [ + { + file: "index", + luaPattern: "____exports.myNumber = getNumber(", + typeScriptPattern: "const myNumber = getNumber(", + }, + { + file: "largeFile", + luaPattern: "local Calculator = __TS__Class()", + typeScriptPattern: "abstract class Calculator", + }, + { + file: "largeFile", + luaPattern: "local CalculatorMul = __TS__Class()", + typeScriptPattern: "class CalculatorMul extends Calculator {", + }, + { + file: "largeFile", + luaPattern: "local function resolveCalculatorClass(", + typeScriptPattern: "function resolveCalculatorClass(", + }, + { + file: "largeFile", + luaPattern: "function ____exports.getNumber(", + typeScriptPattern: "export function getNumber(", + }, + { + file: "largeFile", + luaPattern: 'Error,\n "Unknown operation "', + typeScriptPattern: "throw new Error(", + }, + ]; + + for (const { file: currentFile, luaPattern, typeScriptPattern } of assertPatterns) { + const luaPosition = lineAndColumnOf(transpiledLua, luaPattern); + const mappedLine: { file: string; line: number } = mainSourceMap[luaPosition.line.toString()]; + + const typescriptPosition = lineAndColumnOf(code[currentFile], typeScriptPattern); + expect(mappedLine.line).toEqual(typescriptPosition.line); + expect(mappedLine.file).toBe(`${currentFile}.ts`); + } + }); +}); diff --git a/test/transpile/bundle/bundle-source-maps/index.ts b/test/transpile/bundle/bundle-source-maps/index.ts new file mode 100644 index 000000000..22a2f499e --- /dev/null +++ b/test/transpile/bundle/bundle-source-maps/index.ts @@ -0,0 +1,8 @@ +import { getNumber, Operation } from "./largeFile"; + +// Local variables +const left = getNumber(3, 4, Operation.MUL); +const right = getNumber(5, 6, Operation.SUM); + +export const myNumber = getNumber(left, right, Operation.MUL); +export const sourceMap = (globalThis as any).__TS__sourcemap; diff --git a/test/transpile/bundle/bundle-source-maps/largeFile.ts b/test/transpile/bundle/bundle-source-maps/largeFile.ts new file mode 100644 index 000000000..ae0a9be7a --- /dev/null +++ b/test/transpile/bundle/bundle-source-maps/largeFile.ts @@ -0,0 +1,53 @@ +// Some comments here to check source map correctness +// Some comments here to check source map correctness + +abstract class Calculator { + protected left: number; + protected right: number; + + constructor(left: number, right: number) { + this.left = left; + this.right = right; + } + + public abstract calc(): number; +} + +/** + * Sums two numbers + */ +class CalculatorSum extends Calculator { + public calc(): number { + return this.left + this.right; + } +} + +class CalculatorMul extends Calculator { + public calc(): number { + return this.left * this.right; + } +} + +// Some comments here to check source map correctness + +export const enum Operation { + SUM = "SUM", + MUL = "MUL", +} + +// Local internal function +function resolveCalculatorClass(left: number, right: number, operation: Operation): Calculator { + if (operation === Operation.MUL) { + return new CalculatorMul(left, right); + } + if (operation === Operation.SUM) { + return new CalculatorSum(left, right); + } + + throw new Error(`Unknown operation ${operation}`); +} + +// Some comments here to check source map correctness +export function getNumber(left: number, right: number, operation: Operation): number { + return resolveCalculatorClass(left, right, operation).calc(); +} diff --git a/test/transpile/bundle/bundle-source-maps/tsconfig.json b/test/transpile/bundle/bundle-source-maps/tsconfig.json new file mode 100644 index 000000000..6614ac5d7 --- /dev/null +++ b/test/transpile/bundle/bundle-source-maps/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "target": "esnext", + "lib": ["esnext"], + "types": ["lua-types/5.3"], + "rootDir": "." + }, + "tstl": { + "luaBundle": "bundle.lua", + "luaBundleEntry": "index.ts", + "sourceMapTraceback": true + }, + "include": ["."] +} diff --git a/test/transpile/bundle/bundle-two-files/index.ts b/test/transpile/bundle/bundle-two-files/index.ts new file mode 100644 index 000000000..f189ac088 --- /dev/null +++ b/test/transpile/bundle/bundle-two-files/index.ts @@ -0,0 +1,3 @@ +import { getNumber } from "./otherFile"; + +export const myNumber = getNumber(); diff --git a/test/transpile/bundle/bundle-two-files/otherFile.ts b/test/transpile/bundle/bundle-two-files/otherFile.ts new file mode 100644 index 000000000..ad9eaceb7 --- /dev/null +++ b/test/transpile/bundle/bundle-two-files/otherFile.ts @@ -0,0 +1,3 @@ +export function getNumber(): number { + return 3; +} diff --git a/test/transpile/bundle/bundle-two-files/tsconfig.json b/test/transpile/bundle/bundle-two-files/tsconfig.json new file mode 100644 index 000000000..71060927d --- /dev/null +++ b/test/transpile/bundle/bundle-two-files/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + }, + "tstl": { + "luaBundle": "bundle.lua", + "luaBundleEntry": "index.ts" + }, + "include": ["."] +} diff --git a/test/transpile/directories.spec.ts b/test/transpile/directories.spec.ts new file mode 100644 index 000000000..9572c1e0d --- /dev/null +++ b/test/transpile/directories.spec.ts @@ -0,0 +1,32 @@ +import * as path from "path"; +import * as ts from "typescript"; +import * as tstl from "../../src"; +import { transpileFilesResult } from "./run"; + +interface DirectoryTestCase { + name: string; + options: tstl.CompilerOptions; +} + +test.each([ + { name: "basic", options: {} }, + { name: "basic", options: { outDir: "out" } }, + { name: "basic", options: { rootDir: "src" } }, + { name: "basic", options: { rootDir: "src", outDir: "out" } }, +])("should be able to resolve (%p)", ({ name, options: compilerOptions }) => { + const projectPath = path.join(__dirname, "directories", name); + jest.spyOn(process, "cwd").mockReturnValue(projectPath); + + const config = { + compilerOptions: { ...compilerOptions, types: [], skipLibCheck: true }, + tstl: { luaTarget: tstl.LuaTarget.LuaJIT }, + }; + + const { fileNames, options } = tstl.updateParsedConfigFile( + ts.parseJsonConfigFileContent(config, ts.sys, projectPath) + ); + + const { diagnostics, emittedFiles } = transpileFilesResult(fileNames, options); + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles.map(f => f.name).sort()).toMatchSnapshot(); +}); diff --git a/test/transpile/directories/basic/src/lib/file.ts b/test/transpile/directories/basic/src/lib/file.ts new file mode 100644 index 000000000..4ac2b8fe0 --- /dev/null +++ b/test/transpile/directories/basic/src/lib/file.ts @@ -0,0 +1 @@ +const foo = true; diff --git a/test/compiler/projects/basic/test_src/main.ts b/test/transpile/directories/basic/src/main.ts similarity index 100% rename from test/compiler/projects/basic/test_src/main.ts rename to test/transpile/directories/basic/src/main.ts diff --git a/test/transpile/lualib.spec.ts b/test/transpile/lualib.spec.ts new file mode 100644 index 000000000..20288bd8a --- /dev/null +++ b/test/transpile/lualib.spec.ts @@ -0,0 +1,31 @@ +import * as ts from "typescript"; +import { LuaLibFeature, LuaTarget } from "../../src"; +import { readLuaLibFeature } from "../../src/LuaLib"; +import * as util from "../util"; + +test.each(Object.entries(LuaLibFeature))("Lualib does not use ____exports (%p)", (_, feature) => { + const lualibCode = readLuaLibFeature(feature, LuaTarget.Lua54, ts.sys); + + const exportsOccurrences = lualibCode.match(/____exports/g); + expect(exportsOccurrences).toBeNull(); +}); + +test("Lualib bundle does not assign globals", () => { + // language=TypeScript + util.testModule` + declare const _G: LuaTable; + declare const require: (this: void, module: string) => any; + const globalKeys = new LuaTable(); + for (const [key] of _G) { + globalKeys[key] = true; + } + require("lualib_bundle"); + for (const [key] of _G) { + if (!globalKeys[key]) { + error("Global was assigned: " + key); + } + } + ` + .withLanguageExtensions() + .expectNoExecutionError(); +}); diff --git a/test/transpile/module-resolution.spec.ts b/test/transpile/module-resolution.spec.ts new file mode 100644 index 000000000..ff29ac9b5 --- /dev/null +++ b/test/transpile/module-resolution.spec.ts @@ -0,0 +1,740 @@ +import * as path from "path"; +import * as tstl from "../../src"; +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"); + + const projectWithNodeModules = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")); + + test("can resolve global dependencies with declarations", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().globalWithDeclarationsResults).toEqual({ + foo: "foo from lua global with decls", + bar: "bar from lua global with decls: global with declarations!", + baz: "baz from lua global with decls", + }); + }); + + test("can resolve global dependencies with hand-written declarations", () => { + // No declarations in the node_modules directory, but written by hand in project dir + expect(projectWithNodeModules.getLuaExecutionResult().globalWithoutDeclarationsResults).toEqual({ + foo: "foo from lua global without decls", + bar: "bar from lua global without decls: global without declarations!", + baz: "baz from lua global without decls", + }); + }); + + test("can resolve module dependencies with declarations", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDeclarationsResults).toEqual({ + foo: "foo from lua module with decls", + bar: "bar from lua module with decls: module with declarations!", + baz: "baz from lua module with decls", + }); + }); + + test("can resolve module dependencies with hand-written declarations", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().moduleWithoutDeclarationsResults).toEqual({ + foo: "foo from lua module without decls", + bar: "bar from lua module without decls: module without declarations!", + baz: "baz from lua module without decls", + }); + }); + + test("can resolve package depencency with a dependency on another package", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDependencyResult).toBe( + "Calling dependency: foo from lua module with decls" + ); + }); + + test("resolved package dependency included in bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual({ + globalWithDeclarationsResults: { + foo: "foo from lua global with decls", + bar: "bar from lua global with decls: global with declarations!", + baz: "baz from lua global with decls", + }, + globalWithoutDeclarationsResults: { + foo: "foo from lua global without decls", + bar: "bar from lua global without decls: global without declarations!", + baz: "baz from lua global without decls", + }, + moduleWithDeclarationsResults: { + foo: "foo from lua module with decls", + bar: "bar from lua module with decls: module with declarations!", + baz: "baz from lua module with decls", + }, + moduleWithDependencyResult: "Calling dependency: foo from lua module with decls", + moduleWithoutDeclarationsResults: { + foo: "foo from lua module without decls", + bar: "bar from lua module without decls: module without declarations!", + baz: "baz from lua module without decls", + }, + }); + }); +}); + +describe("module resolution with chained dependencies", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain"); + const expectedResult = { result: "dependency3", result2: "someFunc from otherfile.lua" }; + + test("can resolve dependencies in chain", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .expectToEqual(expectedResult); + }); + + test("resolved package dependency included in bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); + + test("works with different module setting", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ module: ts.ModuleKind.ESNext }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution with outDir", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain"); + const expectedResult = { result: "dependency3", result2: "someFunc from otherfile.lua" }; + + test("emits files in outDir", () => { + const builder = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + + // Get the output paths relative to the project path + const outPaths = builder.getLuaResult().transpiledFiles.map(f => path.relative(projectPath, f.outPath)); + expect(outPaths).toHaveLength(5); + expect(outPaths).toContain(path.join("tstl-out", "main.lua")); + // Note: outputs to lua_modules + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency1", "index.lua")); + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency1", "otherfile.lua")); + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency2", "index.lua")); + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency3", "index.lua")); + }); + + test("emits bundle in outDir", () => { + const mainFile = path.join(projectPath, "main.ts"); + const builder = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ outDir: "tstl-out", luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + + // Get the output paths relative to the project path + const outPaths = builder.getLuaResult().transpiledFiles.map(f => path.relative(projectPath, f.outPath)); + expect(outPaths).toHaveLength(1); + expect(outPaths).toContain(path.join("tstl-out", "bundle.lua")); + }); +}); + +describe("module resolution with sourceDir", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-sourceDir"); + const expectedResult = { + result: "dependency3", + functionInSubDir: "non-node_modules import", + functionReExportedFromSubDir: "nested func result", + nestedFunctionInSubDirOfSubDir: "nested func result", + nestedFunctionUsingFunctionFromParentDir: "nested func: non-node_modules import 2", + }; + + test("can resolve dependencies with sourceDir", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "src", "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + }); + + test("can resolve dependencies and bundle files with sourceDir", () => { + const mainFile = path.join(projectPath, "src", "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1394 + test("can resolve files with non-standard extension (#1394)", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "src", "main.ts")) + .setOptions({ outDir: "tstl-out", extension: ".script" }) + .expectToEqual(expectedResult); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1394 + test("can resolve files with non-standard extension without separator (#1394)", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "src", "main.ts")) + .setOptions({ outDir: "tstl-out", extension: "script" }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution project with lua sources", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-lua-sources"); + const expectedResult = { + funcFromLuaFile: "lua file in subdir", + funcFromSubDirLuaFile: "lua file in subdir", + }; + + test("can resolve lua dependencies", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + }); + + test("can resolve dependencies and bundle files with sourceDir", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution in library mode", () => { + test("result does not contain resolved paths", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain"); + + const { transpiledFiles } = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ buildMode: tstl.BuildMode.Library }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + for (const file of transpiledFiles) { + expect(file.lua).not.toContain('require("lua_modules'); + } + }); + + test("project works in library mode because no external dependencies", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-lua-sources"); + + const { transpiledFiles } = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out", buildMode: tstl.BuildMode.Library }) + .expectToEqual({ + funcFromLuaFile: "lua file in subdir", + funcFromSubDirLuaFile: "lua file in subdir", + }) + .getLuaResult(); + + for (const file of transpiledFiles) { + expect(file.lua).not.toContain('require("lua_modules'); + } + }); +}); + +describe("module resolution project with dependencies built by tstl library mode", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-tstl-library-modules"); + + // First compile dependencies into node_modules. NOTE: Actually writing to disk, very slow + tstl.transpileProject(path.join(projectPath, "dependency1-ts", "tsconfig.json")); + tstl.transpileProject(path.join(projectPath, "dependency2-ts", "tsconfig.json")); + + const expectedResult = { + dependency1IndexResult: "function in dependency 1 index: dependency1OtherFileFunc in dependency1/d1otherfile", + dependency1OtherFileFuncResult: "dependency1OtherFileFunc in dependency1/d1otherfile", + dependency2MainResult: "dependency 2 main", + dependency2OtherFileResult: "Dependency 2 func: my string argument", + }; + + test("can resolve lua dependencies", () => { + const transpileResult = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult) + .getLuaResult(); + + // Assert node_modules file requires the correct lualib_bundle + const requiringLuaFile = path.join("lua_modules", "dependency1", "index.lua"); + const lualibRequiringFile = transpileResult.transpiledFiles.find(f => f.outPath.endsWith(requiringLuaFile)); + expect(lualibRequiringFile).toBeDefined(); + expect(lualibRequiringFile?.lua).toContain('require("lualib_bundle")'); + }); + + test("can resolve dependencies and bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution project with dependencies built by tstl library mode and has exports field", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-tstl-library-has-exports-field"); + + // First compile dependencies into node_modules. NOTE: Actually writing to disk, very slow + const dependency1Path = path.join(projectPath, "node_modules", "dependency1"); + tstl.transpileProject(path.join(dependency1Path, "tsconfig.json")); + + const expectedResult = { + dependency1IndexResult: "function in dependency 1 index: dependency1OtherFileFunc in dependency1/d1otherfile", + dependency1OtherFileFuncResult: "dependency1OtherFileFunc in dependency1/d1otherfile", + }; + + test("can resolve lua dependencies", () => { + const transpileResult = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ + outDir: "tstl-out", + moduleResolution: ts.ModuleResolutionKind.Node16, + module: ts.ModuleKind.Node16, + }) + .expectToEqual(expectedResult) + .getLuaResult(); + + // Assert node_modules file requires the correct lualib_bundle + const requiringLuaFile = path.join("lua_modules", "dependency1", "dist", "index.lua"); + const lualibRequiringFile = transpileResult.transpiledFiles.find(f => f.outPath.endsWith(requiringLuaFile)); + expect(lualibRequiringFile).toBeDefined(); + expect(lualibRequiringFile?.lua).toContain('require("lualib_bundle")'); + }); + + test("can resolve dependencies and bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ + luaBundle: "bundle.lua", + luaBundleEntry: mainFile, + moduleResolution: ts.ModuleResolutionKind.Node16, + module: ts.ModuleKind.Node16, + }) + .expectToEqual(expectedResult); + }); +}); + +// Test fix for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1037 +describe("module resolution with tsx", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-tsx"); + + test("project with tsx files", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.tsx")) + .expectToEqual({ + result: "hello from other.tsx", + indexResult: "hello from dir/index.tsx", + }); + }); +}); + +describe("dependency with complicated inner structure", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-complicated-dependency"); + const tsConfigPath = path.join(projectPath, "tsconfig.json"); + const mainFilePath = path.join(projectPath, "main.ts"); + + const expectedResult = { + otherFileResult: "someFunc from otherfile.lua", + otherFileUtil: "util", + subsubresult: "result from subsub dir", + utilResult: "util", + subdirwithInitResult: "a", + }; + + // Test fix for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1055 + test("bundle should not contain duplicate files", () => { + const mainFile = path.join(projectPath, "main.ts"); + const { transpiledFiles } = util + .testProject(tsConfigPath) + .setMainFileName(mainFilePath) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult) + .getLuaResult(); + + expect(transpiledFiles).toHaveLength(1); + const lua = transpiledFiles[0].lua!; + // util is used in 2 places, but should only occur once in the bundle + const utilModuleOccurrences = (lua.match(/\["lua_modules\.dependency1\.util"\]/g) ?? []).length; + expect(utilModuleOccurrences).toBe(1); + }); + + // Test fix for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1054 + test("should be able to resolve dependency files in subdirectories", () => { + util.testProject(tsConfigPath).setMainFileName(mainFilePath).expectToEqual(expectedResult); + }); +}); + +test("module resolution should not try to resolve @noResolution annotation", () => { + util.testModule` + import * as json from "json"; + const test = json.decode("{}"); + ` + .addExtraFile( + "json.d.ts", + ` + /** @noResolution */ + declare module "json" { + function encode(this: void, data: unknown): string; + function decode(this: void, data: string): unknown; + } + ` + ) + .expectToHaveNoDiagnostics(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1101 +test("module resolution inline require of environment library workaround", () => { + util.testModule` + declare function require(this: void, module: string): any; + + const test = require("@NoResolution:luasource"); + test.foo(); + `.expectToHaveNoDiagnostics(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1118 +describe("module resolution should not try to resolve modules in noResolvePaths", () => { + test("as used in direct import", () => { + util.testModule` + import * as lua from "directimport"; + lua.foo(); + ` + .addExtraFile( + "directimport.d.ts", + `declare module "directimport" { + export function foo(): void; + }` + ) + .setOptions({ noResolvePaths: ["directimport"] }) + .expectToHaveNoDiagnostics(); + }); + + test("as used in imported lua sources", () => { + util.testModule` + import * as lua from "./luasource"; + lua.foo(); + ` + .addExtraFile("luasource.d.ts", "export function foo(): void;") + .addExtraFile( + "luasource.lua", + ` + require("dontResolveThis") + require("a.b.c.foo") + + return { foo = function() return "bar" end } + ` + ) + .setOptions({ noResolvePaths: ["a.b.c.foo", "somethingExtra", "dontResolveThis"] }) + .expectToHaveNoDiagnostics(); + }); + + test("can ignore specific files with glob pattern", () => { + util.testModule` + // Pre-Load as to not error out at runtime + import "preload"; + + import "ignoreme"; + import * as b from "./actualfile"; + + export const result = b.foo(); + ` + .addExtraFile("preload.lua", 'package.preload["ignoreme"] = function() return nil end') + .addExtraFile( + "actualfile.ts", + `export function foo() + { + return 'foo'; + }` + ) + .addExtraFile( + "ignoreme.d.ts", + `declare module "ignoreme" { + export function foo(): void; + }` + ) + .setOptions({ noResolvePaths: ["ignore*"] }) + .expectToHaveNoDiagnostics() + .expectToEqual({ result: "foo" }); + }); + + test("can ignore all files with glob pattern in require", () => { + util.testModule` + declare function require(this: void, module: string): any; + + const a = require("a") + const b = require("b/b") + const c = require("c/c/c") + const d = require("!:?somefile") + ` + .setOptions({ noResolvePaths: ["**"] }) + .expectToHaveNoDiagnostics(); + }); + + test("can ignore all files with glob pattern as used in imported lua sources", () => { + util.testModule` + import * as lua from "./luasource"; + lua.foo(); + ` + .addExtraFile("luasource.d.ts", "export function foo(): void;") + .addExtraFile( + "luasource.lua", + ` + require("ignoreme!") + require("i.g.n.o.r.e") + ` + ) + .setOptions({ noResolvePaths: ["**"] }) + .expectToHaveNoDiagnostics(); + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1062 +test("module resolution should not rewrite @NoResolution requires in library mode", () => { + const { transpiledFiles } = util.testModule` + import * as json from "json"; + const test = json.decode("{}"); + ` + .addExtraFile( + "json.d.ts", + ` + /** @noResolution */ + declare module "json" { + function encode(this: void, data: unknown): string; + function decode(this: void, data: string): unknown; + } + ` + ) + .setOptions({ buildMode: BuildMode.Library }) + .getLuaResult(); + + expect(transpiledFiles).toHaveLength(1); + expect(transpiledFiles[0].lua).toContain('require("@NoResolution:'); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1050 +test("module resolution should not try to resolve require-like functions", () => { + util.testModule` + function custom_require(this: void, value: string) { + return value; + } + + namespace ns { + export function require(this: void, value: string) { + return value; + } + } + + class MyClass { + require(value: string) { + return value; + } + } + const inst = new MyClass(); + + export const result = [ + custom_require("value 1"), + ns.require("value 2"), + inst.require("value 3") + ]; + + ` + .expectToHaveNoDiagnostics() + .expectToEqual({ + result: ["value 1", "value 2", "value 3"], + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1050 +test("module resolution uses baseURL to resolve imported files", () => { + util.testModule` + import { foo } from "dep1"; + import { bar } from "dep2"; + import { baz } from "luadep"; + + export const fooResult = foo(); + export const barResult = bar(); + export const bazResult = baz(); + ` + .addExtraFile( + "myproject/mydeps/dep1.ts", + ` + export function foo() { return "foo"; } + ` + ) + .addExtraFile( + "myproject/mydeps/dep2.ts", + ` + export function bar() { return "bar"; } + ` + ) + .addExtraFile( + "myproject/mydeps/luadep.d.ts", + ` + export function baz(): string; + ` + ) + .addExtraFile( + "myproject/mydeps/luadep.lua", + ` + return { baz = function() return "baz" end } + ` + ) + .setOptions({ baseUrl: "./myproject/mydeps" }) + .expectToEqual({ + fooResult: "foo", + barResult: "bar", + bazResult: "baz", + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1071 +test("includes lualib_bundle when external lua requests it", () => { + util.testModule` + export { foo } from "./lualibuser"; + ` + .addExtraFile( + "lualibuser.d.ts", + ` + export const foo: string[]; + ` + ) + .addExtraFile( + "lualibuser.lua", + ` + local ____lualib = require("lualib_bundle") + local __TS__ArrayPush = ____lualib.__TS__ArrayPush + + local result = {} + __TS__ArrayPush(result, "foo") + __TS__ArrayPush(result, "bar") + + return { foo = result } + ` + ) + .expectToEqual({ + foo: ["foo", "bar"], + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1146 +test("require matches correct pattern", () => { + util.testModule` + declare function require(this: void, module: string): any; + export const addResult = require("a").foo + require("b").foo; + export const callResult = require("c")("foo"); + ` + .addExtraFile("a.lua", "return { foo = 3 }") + .addExtraFile("b.lua", "return { foo = 5 }") + .addExtraFile("c.lua", "return function(self, a) return a end") + .expectToEqual({ addResult: 3 + 5, callResult: "foo" }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1307 +test("lualib_module with parent directory import (#1307)", () => { + const projectDir = path.join(__dirname, "module-resolution", "project-with-dependency-with-same-file-names"); + const inputProject = path.join(projectDir, "tsconfig.json"); + + util.testProject(inputProject).setMainFileName(path.join(projectDir, "index.ts")).expectToEqual({ + // eslint-disable-next-line @typescript-eslint/naming-convention + BASE_CONSTANT: 123, + // eslint-disable-next-line @typescript-eslint/naming-convention + FEATURE_CONSTANT: 456, + }); +}); + +test("supports paths configuration", () => { + // Package root + const baseProjectPath = path.resolve(__dirname, "module-resolution", "paths-simple"); + // myprogram package + const projectPath = path.join(baseProjectPath, "myprogram"); + const projectTsConfig = path.join(projectPath, "tsconfig.json"); + const mainFile = path.join(projectPath, "main.ts"); + + const luaResult = util + .testProject(projectTsConfig) + .setMainFileName(mainFile) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + expect(snapshotPaths(luaResult.transpiledFiles)).toMatchSnapshot(); + + // Bundle to have all files required to execute and check result + util.testProject(projectTsConfig) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual({ foo: 314, bar: 271 }); +}); + +test("supports complicated paths configuration", () => { + // Package root + const baseProjectPath = path.resolve(__dirname, "module-resolution", "paths-base-tsconfig"); + // myprogram package + const projectPath = path.join(baseProjectPath, "packages", "myprogram"); + const projectTsConfig = path.join(projectPath, "tsconfig.json"); + const mainFile = path.join(projectPath, "src", "main.ts"); + + const luaResult = util + .testProject(projectTsConfig) + .setMainFileName(mainFile) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + expect(snapshotPaths(luaResult.transpiledFiles)).toMatchSnapshot(); + + // Bundle to have all files required to execute + util.testProject(projectTsConfig) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual({ foo: 314, bar: 271 }); +}); + +test("paths without baseUrl is error", () => { + util.testFunction``.setOptions({ paths: {} }).expectToHaveDiagnostics([pathsWithoutBaseUrl.code]); +}); + +test("module resolution using plugin", () => { + const baseProjectPath = path.resolve(__dirname, "module-resolution", "project-with-module-resolution-plugin"); + const projectTsConfig = path.join(baseProjectPath, "tsconfig.json"); + const mainFile = path.join(baseProjectPath, "main.ts"); + + const testBuilder = util + .testProject(projectTsConfig) + .setMainFileName(mainFile) + .setOptions({ + luaPlugins: [ + { + name: path.join(__dirname, "./plugins/moduleResolution.ts"), + }, + ], + }) + .expectToHaveNoDiagnostics(); + + const luaResult = testBuilder.getLuaResult(); + + expect(luaResult.transpiledFiles).toHaveLength(3); + + testBuilder.expectToEqual({ result: ["foo", "absolutefoo"] }); +}); + +function snapshotPaths(files: tstl.TranspiledFile[]) { + return files.map(f => normalizeSlashes(f.outPath).split("module-resolution")[1]).sort(); +} diff --git a/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/src/bar.ts b/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/src/bar.ts new file mode 100644 index 000000000..fef4e9ed6 --- /dev/null +++ b/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/src/bar.ts @@ -0,0 +1 @@ +export const bar = 271; diff --git a/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/src/index.ts b/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/src/index.ts new file mode 100644 index 000000000..c932fa9f5 --- /dev/null +++ b/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/src/index.ts @@ -0,0 +1 @@ +export const foo = 314; diff --git a/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/tsconfig.json b/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/tsconfig.json new file mode 100644 index 000000000..5ac343336 --- /dev/null +++ b/test/transpile/module-resolution/paths-base-tsconfig/packages/mypackage/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/packages/tstl-module" + }, + "include": ["./src/**/*.ts"] +} diff --git a/test/transpile/module-resolution/paths-base-tsconfig/packages/myprogram/src/main.ts b/test/transpile/module-resolution/paths-base-tsconfig/packages/myprogram/src/main.ts new file mode 100644 index 000000000..973b93922 --- /dev/null +++ b/test/transpile/module-resolution/paths-base-tsconfig/packages/myprogram/src/main.ts @@ -0,0 +1,4 @@ +import { foo } from "mypackage"; +import { bar } from "mypackage/bar"; + +export { foo, bar }; diff --git a/test/transpile/module-resolution/paths-base-tsconfig/packages/myprogram/tsconfig.json b/test/transpile/module-resolution/paths-base-tsconfig/packages/myprogram/tsconfig.json new file mode 100644 index 000000000..a0ba7c18e --- /dev/null +++ b/test/transpile/module-resolution/paths-base-tsconfig/packages/myprogram/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/packages/tstl-program" + }, + "include": ["./src/**/*.ts"] +} diff --git a/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json b/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json new file mode 100644 index 000000000..3f961c78a --- /dev/null +++ b/test/transpile/module-resolution/paths-base-tsconfig/tsconfig.base.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": { + "mypackage": ["packages/mypackage/src/index.ts"], + "mypackage/*": ["packages/mypackage/src/*"] + } + } +} diff --git a/test/transpile/module-resolution/paths-simple/mypackage/bar.ts b/test/transpile/module-resolution/paths-simple/mypackage/bar.ts new file mode 100644 index 000000000..fef4e9ed6 --- /dev/null +++ b/test/transpile/module-resolution/paths-simple/mypackage/bar.ts @@ -0,0 +1 @@ +export const bar = 271; diff --git a/test/transpile/module-resolution/paths-simple/mypackage/index.ts b/test/transpile/module-resolution/paths-simple/mypackage/index.ts new file mode 100644 index 000000000..c932fa9f5 --- /dev/null +++ b/test/transpile/module-resolution/paths-simple/mypackage/index.ts @@ -0,0 +1 @@ +export const foo = 314; diff --git a/test/transpile/module-resolution/paths-simple/mypackage/tsconfig.json b/test/transpile/module-resolution/paths-simple/mypackage/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/transpile/module-resolution/paths-simple/mypackage/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/test/transpile/module-resolution/paths-simple/myprogram/main.ts b/test/transpile/module-resolution/paths-simple/myprogram/main.ts new file mode 100644 index 000000000..e7687877a --- /dev/null +++ b/test/transpile/module-resolution/paths-simple/myprogram/main.ts @@ -0,0 +1,4 @@ +import { foo } from "myOtherPackage"; +import { bar } from "myOtherPackage/bar"; + +export { foo, bar }; diff --git a/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json b/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json new file mode 100644 index 000000000..f01271ac9 --- /dev/null +++ b/test/transpile/module-resolution/paths-simple/myprogram/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "paths": { + "myOtherPackage": ["../mypackage"], + "myOtherPackage/*": ["../mypackage/*"] + } + } +} diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/main.ts b/test/transpile/module-resolution/project-with-complicated-dependency/main.ts new file mode 100644 index 000000000..70d8123fa --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/main.ts @@ -0,0 +1,7 @@ +import * as dependency1 from "dependency1"; + +export const otherFileResult = dependency1.otherFileFromDependency1(); +export const utilResult = dependency1.callUtil(); +export const otherFileUtil = dependency1.otherFileUtil(); +export const subsubresult = dependency1.subsubdirfileResult; +export const subdirwithInitResult = dependency1.subdirWithInitResult; diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/index.d.ts b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/index.d.ts new file mode 100644 index 000000000..28f4f5baa --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/index.d.ts @@ -0,0 +1,6 @@ +/** @noSelfInFile */ +export declare function otherFileFromDependency1(): string; +export declare function callUtil(): string; +export declare function otherFileUtil(): string; +export const subsubdirfileResult: string; +export const subdirWithInitResult: string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/index.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/index.lua new file mode 100644 index 000000000..c3838b1f4 --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/index.lua @@ -0,0 +1,18 @@ +local otherfile = require("otherfile") +local util = require("util") +local subdirfile = require("subdir.subdirfile") +local othersubdirfile = require("subdir.othersubdirfile") +local subsubdirfile = require("subdir.subsubdir.subsubdirfile") +local subdirwithinit = require("subdirwithinit") + +return { + otherFileFromDependency1 = otherfile.someFunc, + callUtil = function() + return util.utilf() + end, + otherFileUtil = otherfile.callUtil, + subdirfileResult = subdirfile.subdirfileResult, + othersubdirfileResult = othersubdirfile.othersubdirfileResult, + subsubdirfileResult = subsubdirfile.subsubresult, + subdirWithInitResult = subdirwithinit.a() +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/otherfile.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/otherfile.lua new file mode 100644 index 000000000..0fb8719ac --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/otherfile.lua @@ -0,0 +1,10 @@ +local util = require("util") + +return { + someFunc = function() + return "someFunc from otherfile.lua" + end, + callUtil = function() + return util.utilf() + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/othersubdirfile.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/othersubdirfile.lua new file mode 100644 index 000000000..45e5c985f --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/othersubdirfile.lua @@ -0,0 +1,3 @@ +return { + result = "result from other subdirectory file" +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/subdirfile.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/subdirfile.lua new file mode 100644 index 000000000..fe26400a1 --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/subdirfile.lua @@ -0,0 +1,5 @@ +local othersubdirfile = require("subdir.othersubdirfile") +return { + subdirfileResult = "result from subdirectory file!", + othersubdirfileResult = othersubdirfile.result +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/subsubdir/subsubdirfile.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/subsubdir/subsubdirfile.lua new file mode 100644 index 000000000..581c8f858 --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdir/subsubdir/subsubdirfile.lua @@ -0,0 +1,3 @@ +return { + subsubresult = "result from subsub dir" +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdirwithinit/init.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdirwithinit/init.lua new file mode 100644 index 000000000..f26612a6f --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/subdirwithinit/init.lua @@ -0,0 +1,3 @@ +return { + a = function() return "a" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/util.lua b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/util.lua new file mode 100644 index 000000000..e4e812406 --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/node_modules/dependency1/util.lua @@ -0,0 +1,3 @@ +return { + utilf = function() return "util" end, +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-complicated-dependency/tsconfig.json b/test/transpile/module-resolution/project-with-complicated-dependency/tsconfig.json new file mode 100644 index 000000000..b76533290 --- /dev/null +++ b/test/transpile/module-resolution/project-with-complicated-dependency/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + } +} diff --git a/test/transpile/module-resolution/project-with-dependency-chain/main.ts b/test/transpile/module-resolution/project-with-dependency-chain/main.ts new file mode 100644 index 000000000..cda66cd55 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/main.ts @@ -0,0 +1,4 @@ +import * as dependency1 from "dependency1"; + +export const result = dependency1.f1(); +export const result2 = dependency1.otherFileFromDependency1(); diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts new file mode 100644 index 000000000..5155e72c5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +export declare function f1(): string; +export declare function otherFileFromDependency1(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua new file mode 100644 index 000000000..80cae6fb8 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua @@ -0,0 +1,7 @@ +local dependency2 = require("dependency2") +local otherfile = require("otherfile") + +return { + f1 = function() return dependency2.f2() end, + otherFileFromDependency1 = otherfile.someFunc +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/otherfile.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/otherfile.lua new file mode 100644 index 000000000..871964bf7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/otherfile.lua @@ -0,0 +1,5 @@ +return { + someFunc = function() + return "someFunc from otherfile.lua" + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua new file mode 100644 index 000000000..10b36647e --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua @@ -0,0 +1,5 @@ +local dependency3 = require("dependency3") + +return { + f2 = dependency3.f3 +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua new file mode 100644 index 000000000..112f9ba7e --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua @@ -0,0 +1,3 @@ +return { + f3 = function() return "dependency3" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/tsconfig.json b/test/transpile/module-resolution/project-with-dependency-chain/tsconfig.json new file mode 100644 index 000000000..b76533290 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + } +} diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/index.ts b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/index.ts new file mode 100644 index 000000000..0ab21dc4e --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/index.ts @@ -0,0 +1 @@ +export * from "mymodule"; diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/constants.lua b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/constants.lua new file mode 100644 index 000000000..de5917dd7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/constants.lua @@ -0,0 +1,3 @@ +local ____exports = {} +____exports.BASE_CONSTANT = 123 +return ____exports diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/feature/constants.lua b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/feature/constants.lua new file mode 100644 index 000000000..9d3bd6110 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/feature/constants.lua @@ -0,0 +1,3 @@ +local ____exports = {} +____exports.FEATURE_CONSTANT = 456 +return ____exports diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/feature/feature.lua b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/feature/feature.lua new file mode 100644 index 000000000..103496eaa --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/feature/feature.lua @@ -0,0 +1,8 @@ +local ____exports = {} +local ____constants = require("constants") +local BASE_CONSTANT = ____constants.BASE_CONSTANT +local ____constants = require("feature.constants") +local FEATURE_CONSTANT = ____constants.FEATURE_CONSTANT +____exports.BASE_CONSTANT = BASE_CONSTANT +____exports.FEATURE_CONSTANT = FEATURE_CONSTANT +return ____exports diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/index.d.ts b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/index.d.ts new file mode 100644 index 000000000..90401efad --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/index.d.ts @@ -0,0 +1,2 @@ +export declare const BASE_CONSTANT: number; +export declare const FEATURE_CONSTANT: number; diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/index.lua b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/index.lua new file mode 100644 index 000000000..4e57411d3 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/node_modules/mymodule/index.lua @@ -0,0 +1,10 @@ +local ____exports = {} +do + local ____export = require("feature.feature") + for ____exportKey, ____exportValue in pairs(____export) do + if ____exportKey ~= "default" then + ____exports[____exportKey] = ____exportValue + end + end +end +return ____exports diff --git a/test/transpile/module-resolution/project-with-dependency-with-same-file-names/tsconfig.json b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-with-same-file-names/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.d.ts b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.d.ts new file mode 100644 index 000000000..6166280f2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function funcFromSubDir(): string; diff --git a/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.lua b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.lua new file mode 100644 index 000000000..a23007078 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.lua @@ -0,0 +1,3 @@ +return { + funcFromSubDir = function() return "lua file in subdir" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-lua-sources/luafile.d.ts b/test/transpile/module-resolution/project-with-lua-sources/luafile.d.ts new file mode 100644 index 000000000..6294675a1 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/luafile.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function funcInLuaFile(): string; diff --git a/test/transpile/module-resolution/project-with-lua-sources/luafile.lua b/test/transpile/module-resolution/project-with-lua-sources/luafile.lua new file mode 100644 index 000000000..56e61090f --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/luafile.lua @@ -0,0 +1,3 @@ +return { + funcInLuaFile = function() return "lua file in subdir" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-lua-sources/main.ts b/test/transpile/module-resolution/project-with-lua-sources/main.ts new file mode 100644 index 000000000..d4c19b440 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/main.ts @@ -0,0 +1,5 @@ +import { funcInLuaFile } from "./luafile"; +import { funcFromSubDir } from "./lua_sources/otherluaFile"; + +export const funcFromLuaFile = funcInLuaFile(); +export const funcFromSubDirLuaFile = funcFromSubDir(); diff --git a/test/transpile/module-resolution/project-with-lua-sources/tsconfig.json b/test/transpile/module-resolution/project-with-lua-sources/tsconfig.json new file mode 100644 index 000000000..a07455ed7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "lib": ["esnext"], + "types": [], + "outDir": "tstl-out" + } +} diff --git a/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/absolutebar.lua b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/absolutebar.lua new file mode 100644 index 000000000..35718ddea --- /dev/null +++ b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/absolutebar.lua @@ -0,0 +1,5 @@ +local ____exports = {} +function ____exports.absolutefoo(self) + return "absolutefoo" +end +return ____exports \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/absolutefoo.d.ts b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/absolutefoo.d.ts new file mode 100644 index 000000000..9df109b2e --- /dev/null +++ b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/absolutefoo.d.ts @@ -0,0 +1 @@ +export declare function absolutefoo(): string; diff --git a/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/bar.lua b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/bar.lua new file mode 100644 index 000000000..b821a15b5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/bar.lua @@ -0,0 +1,5 @@ +local ____exports = {} +function ____exports.foo(self) + return "foo" +end +return ____exports \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/foo.d.ts b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/foo.d.ts new file mode 100644 index 000000000..27e069e94 --- /dev/null +++ b/test/transpile/module-resolution/project-with-module-resolution-plugin/lua_sources/foo.d.ts @@ -0,0 +1 @@ +export declare function foo(): string; diff --git a/test/transpile/module-resolution/project-with-module-resolution-plugin/main.ts b/test/transpile/module-resolution/project-with-module-resolution-plugin/main.ts new file mode 100644 index 000000000..e3e6bc616 --- /dev/null +++ b/test/transpile/module-resolution/project-with-module-resolution-plugin/main.ts @@ -0,0 +1,3 @@ +import { foo } from "./lua_sources/foo"; +import { absolutefoo } from "./lua_sources/absolutefoo"; +export const result = [foo(), absolutefoo()]; diff --git a/test/transpile/module-resolution/project-with-module-resolution-plugin/tsconfig.json b/test/transpile/module-resolution/project-with-module-resolution-plugin/tsconfig.json new file mode 100644 index 000000000..06ed18c9f --- /dev/null +++ b/test/transpile/module-resolution/project-with-module-resolution-plugin/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "lib": ["esnext"], + "types": [] + } +} diff --git a/test/transpile/module-resolution/project-with-node-modules/lua-global-without-decls.d.ts b/test/transpile/module-resolution/project-with-node-modules/lua-global-without-decls.d.ts new file mode 100644 index 000000000..4d1c1db1b --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/lua-global-without-decls.d.ts @@ -0,0 +1,4 @@ +/** @noSelfInFile */ +declare function fooGlobalWithoutDecls(): string; +declare function barGlobalWithoutDecls(param: string): string; +declare function bazGlobalWithoutDecls(): string; diff --git a/test/transpile/module-resolution/project-with-node-modules/lua-module-without-decls.d.ts b/test/transpile/module-resolution/project-with-node-modules/lua-module-without-decls.d.ts new file mode 100644 index 000000000..9ccfc9bfe --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/lua-module-without-decls.d.ts @@ -0,0 +1,9 @@ +/** @noSelfInFile */ +declare module "lua-module-without-decls" { + function foo(this: void): string; + function bar(this: void, param: string): string; +} + +declare module "lua-module-without-decls/baz" { + function baz(this: void): string; +} diff --git a/test/transpile/module-resolution/project-with-node-modules/main.ts b/test/transpile/module-resolution/project-with-node-modules/main.ts new file mode 100644 index 000000000..8d9332190 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/main.ts @@ -0,0 +1,39 @@ +import "lua-global-with-decls"; +import "lua-global-with-decls/baz"; + +import "lua-global-without-decls"; +import "lua-global-without-decls/baz"; + +import * as moduleWithDeclarations from "lua-module-with-decls"; +import * as moduleWithDeclarationsBaz from "lua-module-with-decls/baz"; + +import * as moduleWithoutDeclarations from "lua-module-without-decls"; +import * as moduleWithoutDeclarationsBaz from "lua-module-without-decls/baz"; + +import * as moduleWithDependency from "lua-module-with-dependency"; + +export const globalWithDeclarationsResults = { + foo: fooGlobal(), + bar: barGlobal("global with declarations!"), + baz: bazGlobal(), +}; + +export const globalWithoutDeclarationsResults = { + foo: fooGlobalWithoutDecls(), + bar: barGlobalWithoutDecls("global without declarations!"), + baz: bazGlobalWithoutDecls(), +}; + +export const moduleWithDeclarationsResults = { + foo: moduleWithDeclarations.foo(), + bar: moduleWithDeclarations.bar("module with declarations!"), + baz: moduleWithDeclarationsBaz.baz(), +}; + +export const moduleWithoutDeclarationsResults = { + foo: moduleWithoutDeclarations.foo(), + bar: moduleWithoutDeclarations.bar("module without declarations!"), + baz: moduleWithoutDeclarationsBaz.baz(), +}; + +export const moduleWithDependencyResult = moduleWithDependency.callDependency(); diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.d.ts new file mode 100644 index 000000000..23f55b1ad --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.d.ts @@ -0,0 +1 @@ +declare function bazGlobal(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.lua new file mode 100644 index 000000000..952807f1a --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.lua @@ -0,0 +1,3 @@ +function bazGlobal() + return "baz from lua global with decls" +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.d.ts new file mode 100644 index 000000000..1339056b5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +declare function fooGlobal(): string; +declare function barGlobal(param: string): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.lua new file mode 100644 index 000000000..d10edd189 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.lua @@ -0,0 +1,6 @@ +function fooGlobal() + return "foo from lua global with decls" +end +function barGlobal(param) + return "bar from lua global with decls: " .. param +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/baz.lua new file mode 100644 index 000000000..02980e662 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/baz.lua @@ -0,0 +1,3 @@ +function bazGlobalWithoutDecls() + return "baz from lua global without decls" +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/index.lua new file mode 100644 index 000000000..46dd970a0 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/index.lua @@ -0,0 +1,6 @@ +function fooGlobalWithoutDecls() + return "foo from lua global without decls" +end +function barGlobalWithoutDecls(param) + return "bar from lua global without decls: " .. param +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.d.ts new file mode 100644 index 000000000..5f46a6ce2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function baz(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.lua new file mode 100644 index 000000000..f4e16a0d7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.lua @@ -0,0 +1,5 @@ +return { + baz = function() + return "baz from lua module with decls" + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.d.ts new file mode 100644 index 000000000..aa83ce4a6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +export declare function foo(): string; +export declare function bar(param: string): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.lua new file mode 100644 index 000000000..0c5d47fe6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.lua @@ -0,0 +1,8 @@ +return { + foo = function() + return "foo from lua module with decls" + end, + bar = function(param) + return "bar from lua module with decls: " .. param + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.d.ts new file mode 100644 index 000000000..381596acd --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.d.ts @@ -0,0 +1,2 @@ +/** @noSelf */ +export declare function callDependency(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.lua new file mode 100644 index 000000000..9bc475e5b --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.lua @@ -0,0 +1,7 @@ +local dependency = require("lua-module-with-decls") + +return { + callDependency = function() + return "Calling dependency: " .. dependency.foo() + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/baz.lua new file mode 100644 index 000000000..7358f341b --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/baz.lua @@ -0,0 +1,5 @@ +return { + baz = function() + return "baz from lua module without decls" + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/index.lua new file mode 100644 index 000000000..e73581589 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/index.lua @@ -0,0 +1,8 @@ +return { + foo = function() + return "foo from lua module without decls" + end, + bar = function(param) + return "bar from lua module without decls: " .. param + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/tsconfig.json b/test/transpile/module-resolution/project-with-node-modules/tsconfig.json new file mode 100644 index 000000000..935b64af6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + } +} diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.d.ts b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.d.ts new file mode 100644 index 000000000..761d6bc02 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function f1(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.lua b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.lua new file mode 100644 index 000000000..dcf28d6fd --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.lua @@ -0,0 +1,5 @@ +local dependency2 = require("dependency2") + +return { + f1 = function() return dependency2.f2() end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency2/index.lua b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency2/index.lua new file mode 100644 index 000000000..10b36647e --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency2/index.lua @@ -0,0 +1,5 @@ +local dependency3 = require("dependency3") + +return { + f2 = dependency3.f3 +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency3/index.lua b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency3/index.lua new file mode 100644 index 000000000..112f9ba7e --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency3/index.lua @@ -0,0 +1,3 @@ +return { + f3 = function() return "dependency3" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/main.ts b/test/transpile/module-resolution/project-with-sourceDir/src/main.ts new file mode 100644 index 000000000..9014f7b62 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/main.ts @@ -0,0 +1,9 @@ +import * as dependency1 from "dependency1"; +import { func, nestedFunc } from "./subdir/otherfile"; +import { nestedFunc as nestedFuncOriginal, nestedFuncUsingParent } from "./subdir/subdirofsubdir/nestedfile"; + +export const result = dependency1.f1(); +export const functionInSubDir = func(); +export const functionReExportedFromSubDir = nestedFunc(); +export const nestedFunctionInSubDirOfSubDir = nestedFuncOriginal(); +export const nestedFunctionUsingFunctionFromParentDir = nestedFuncUsingParent(); diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile.ts b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile.ts new file mode 100644 index 000000000..c12dee955 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile.ts @@ -0,0 +1,5 @@ +export function func() { + return "non-node_modules import"; +} + +export { nestedFunc } from "./subdirofsubdir/nestedfile"; diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile2.ts b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile2.ts new file mode 100644 index 000000000..1132c2ea1 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile2.ts @@ -0,0 +1,3 @@ +export function func2() { + return "non-node_modules import 2"; +} diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/subdir/subdirofsubdir/nestedfile.ts b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/subdirofsubdir/nestedfile.ts new file mode 100644 index 000000000..d316f023e --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/subdirofsubdir/nestedfile.ts @@ -0,0 +1,9 @@ +import { func2 } from "../otherfile2"; + +export function nestedFunc() { + return "nested func result"; +} + +export function nestedFuncUsingParent() { + return `nested func: ${func2()}`; +} diff --git a/test/transpile/module-resolution/project-with-sourceDir/tsconfig.json b/test/transpile/module-resolution/project-with-sourceDir/tsconfig.json new file mode 100644 index 000000000..200df2468 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "src" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/main.ts b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/main.ts new file mode 100644 index 000000000..3421911f2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/main.ts @@ -0,0 +1,5 @@ +import { dependency1IndexFunc } from "dependency1/sub"; +import { dependency1OtherFileFunc } from "dependency1/sub/d1otherfile"; + +export const dependency1IndexResult = dependency1IndexFunc(); +export const dependency1OtherFileFuncResult = dependency1OtherFileFunc(); diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/package.json b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/package.json new file mode 100644 index 000000000..c74b862a1 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/package.json @@ -0,0 +1,14 @@ +{ + "name": "dependency1", + "version": "0.0.1", + "exports": { + "./sub": { + "require": "./dist/index", + "types": "./dist/index.d.ts" + }, + "./sub/*": { + "require": "./dist/*", + "types": "./dist/*.d.ts" + } + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/src/d1otherfile.ts b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/src/d1otherfile.ts new file mode 100644 index 000000000..3877a1c52 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/src/d1otherfile.ts @@ -0,0 +1,3 @@ +export function dependency1OtherFileFunc() { + return "dependency1OtherFileFunc in dependency1/d1otherfile"; +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/src/index.ts b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/src/index.ts new file mode 100644 index 000000000..03cff47c5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/src/index.ts @@ -0,0 +1,10 @@ +import { dependency1OtherFileFunc } from "./d1otherfile"; + +export function dependency1IndexFunc() { + return "function in dependency 1 index: " + dependency1OtherFileFunc(); +} + +export function squares(nums: number[]) { + // Require lualib functionality + return nums.map(n => n * n); +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/tsconfig.json new file mode 100644 index 000000000..d6ffcdc0c --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/node_modules/dependency1/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": true + }, + "tstl": { + "buildMode": "library" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/package.json b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/package.json new file mode 100644 index 000000000..b88dc4bc0 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/package.json @@ -0,0 +1,7 @@ +{ + "name": "app", + "version": "0.0.1", + "dependencies": { + "dependency1": "file:../dependency1-ts" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/tsconfig.json new file mode 100644 index 000000000..7b0969155 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-has-exports-field/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "moduleResolution": "node16" + }, + "references": [{ "path": "./node_modules/dependency1" }] +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/d1otherfile.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/d1otherfile.ts new file mode 100644 index 000000000..3877a1c52 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/d1otherfile.ts @@ -0,0 +1,3 @@ +export function dependency1OtherFileFunc() { + return "dependency1OtherFileFunc in dependency1/d1otherfile"; +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/index.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/index.ts new file mode 100644 index 000000000..03cff47c5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/index.ts @@ -0,0 +1,10 @@ +import { dependency1OtherFileFunc } from "./d1otherfile"; + +export function dependency1IndexFunc() { + return "function in dependency 1 index: " + dependency1OtherFileFunc(); +} + +export function squares(nums: number[]) { + // Require lualib functionality + return nums.map(n => n * n); +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/tsconfig.json new file mode 100644 index 000000000..5fcb76fbe --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../node_modules/dependency1", + "declaration": true + }, + "tstl": { + "buildMode": "library" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/d2otherfile.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/d2otherfile.ts new file mode 100644 index 000000000..63e34f8b6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/d2otherfile.ts @@ -0,0 +1,3 @@ +export function dependency2OtherFileFunc(this: void, arg: string) { + return `Dependency 2 func: ${arg}`; +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/main.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/main.ts new file mode 100644 index 000000000..03b90c231 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/main.ts @@ -0,0 +1,5 @@ +export function dependency2Main() { + return "dependency 2 main"; +} + +export * from "./d2otherfile"; diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/tsconfig.json new file mode 100644 index 000000000..f77b336a2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../node_modules/dependency2", + "declaration": true + }, + "tstl": { + "buildMode": "library" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/main.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/main.ts new file mode 100644 index 000000000..c1286088a --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/main.ts @@ -0,0 +1,8 @@ +import { dependency1IndexFunc } from "dependency1"; +import { dependency1OtherFileFunc } from "dependency1/d1otherfile"; +import { dependency2Main, dependency2OtherFileFunc } from "dependency2/main"; + +export const dependency1IndexResult = dependency1IndexFunc(); +export const dependency1OtherFileFuncResult = dependency1OtherFileFunc(); +export const dependency2MainResult = dependency2Main(); +export const dependency2OtherFileResult = dependency2OtherFileFunc("my string argument"); diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-modules/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/test/transpile/module-resolution/project-with-tsx/dir/index.tsx b/test/transpile/module-resolution/project-with-tsx/dir/index.tsx new file mode 100644 index 000000000..20c981b8a --- /dev/null +++ b/test/transpile/module-resolution/project-with-tsx/dir/index.tsx @@ -0,0 +1,3 @@ +export function indexf() { + return "hello from dir/index.tsx"; +} diff --git a/test/transpile/module-resolution/project-with-tsx/main.tsx b/test/transpile/module-resolution/project-with-tsx/main.tsx new file mode 100644 index 000000000..d17e2a7e4 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tsx/main.tsx @@ -0,0 +1,5 @@ +import { f } from "./other"; +import { indexf } from "./dir"; + +export const result = f(); +export const indexResult = indexf(); diff --git a/test/transpile/module-resolution/project-with-tsx/other.tsx b/test/transpile/module-resolution/project-with-tsx/other.tsx new file mode 100644 index 000000000..514705822 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tsx/other.tsx @@ -0,0 +1,3 @@ +export function f() { + return "hello from other.tsx"; +} diff --git a/test/transpile/module-resolution/project-with-tsx/tsconfig.json b/test/transpile/module-resolution/project-with-tsx/tsconfig.json new file mode 100644 index 000000000..b5fbd521b --- /dev/null +++ b/test/transpile/module-resolution/project-with-tsx/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "jsx": "react" + } +} diff --git a/test/transpile/paths.spec.ts b/test/transpile/paths.spec.ts new file mode 100644 index 000000000..f98b6e635 --- /dev/null +++ b/test/transpile/paths.spec.ts @@ -0,0 +1,160 @@ +import * as path from "path"; +import * as ts from "typescript"; +import { getEmitPath, getSourceDir } from "../../src"; +import * as util from "../util"; + +const cwd = process.cwd(); + +// Path for project tsconfig.json to resolve for +const configFilePath = path.join(cwd, "tsconfig.json"); + +describe("getSourceDir", () => { + test("with rootDir", () => { + const program = ts.createProgram(["main.ts", "src/otherfile.ts"], { configFilePath, rootDir: "src" }); + + // If rootdir is specified, rootDir is the sourceDir + expect(getSourceDir(program)).toBe(path.join(cwd, "src")); + }); + + test("without rootDir", () => { + const program = ts.createProgram(["main.ts", "src/otherfile.ts"], { configFilePath }); + + // If rootDir is not specified, root dir is where the config file is + expect(normalize(getSourceDir(program))).toBe(cwd); + }); + + test("without config file in src dir", () => { + const program = ts.createProgram([path.join(cwd, "src", "main.ts"), path.join(cwd, "src", "otherfile.ts")], {}); + + // getCommonSourceDirectory does not work right so mock it + jest.spyOn(program, "getCommonSourceDirectory").mockReturnValue(path.join(cwd, "src")); + + // If there is no config file, return the common source directory + expect(normalize(getSourceDir(program))).toBe(path.join(cwd, "src")); + }); +}); + +describe("getEmitPath", () => { + test("puts files next to input without options", () => { + const { transpiledFiles } = util.testModule`` + .setMainFileName("main.ts") + .addExtraFile("dir/extra.ts", "") + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toContain("main.lua"); + expect(fileNames).toContain(path.join("dir", "extra.lua")); + }); + + test("puts files in outdir", () => { + const outDir = path.join(cwd, "tstl-out"); + const { transpiledFiles } = util.testModule`` + .setMainFileName("main.ts") + .addExtraFile("dir/extra.ts", "") + .setOptions({ outDir }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toContain(path.join(outDir, "main.lua")); + expect(fileNames).toContain(path.join(outDir, "dir", "extra.lua")); + }); + + test("puts files from rootDir in outdir", () => { + const outDir = path.join(cwd, "tstl-out"); + const { transpiledFiles } = util.testModule`` + .setMainFileName("src/main.ts") + .addExtraFile("src/extra.ts", "") + .setOptions({ rootDir: "src", outDir }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toContain(path.join(outDir, "main.lua")); + expect(fileNames).toContain(path.join(outDir, "extra.lua")); + }); + + test("puts bundle relative to project root", () => { + const { transpiledFiles } = util.testModule`` + .setMainFileName("src/main.ts") + .addExtraFile("src/extra.ts", "") + .setOptions({ configFilePath, rootDir: "src", luaBundle: "out/bundle.lua", luaBundleEntry: "src/main.ts" }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toHaveLength(1); + expect(fileNames).toContain(path.join(cwd, "out", "bundle.lua")); + }); + + test("puts bundle relative to outdir", () => { + const { transpiledFiles } = util.testModule`` + .setMainFileName("src/main.ts") + .addExtraFile("src/extra.ts", "") + .setOptions({ + configFilePath, + rootDir: "src", + outDir: "out1", + luaBundle: "out2/bundle.lua", + luaBundleEntry: "src/main.ts", + }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toHaveLength(1); + expect(fileNames).toContain(path.join(cwd, "out1", "out2", "bundle.lua")); + }); + + test.each([".scar", "scar"])("uses config extension (%p)", extension => { + const { transpiledFiles } = util.testModule`` + .setMainFileName("main.ts") + .addExtraFile("dir/extra.ts", "") + .setOptions({ extension }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toContain("main.scar"); + expect(fileNames).toContain(path.join("dir", "extra.scar")); + }); + + test("bundle with different extension", () => { + const { transpiledFiles } = util.testModule`` + .setMainFileName("src/main.ts") + .addExtraFile("src/extra.ts", "") + .setOptions({ + configFilePath, + rootDir: "src", + outDir: "out1", + luaBundle: "out2/bundle.scar", + luaBundleEntry: "src/main.ts", + }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + const fileNames = transpiledFiles.map(f => f.outPath); + expect(fileNames).toHaveLength(1); + expect(fileNames).toContain(path.join(cwd, "out1", "out2", "bundle.scar")); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1540 + test("puts files next to their source if no config is given (#1540)", () => { + const file1 = path.join("src", "main.ts"); + const file2 = path.join("src", "otherfile.ts"); + const file3 = path.join("src", "nested", "nestedfile.ts"); + const program = ts.createProgram([file1, file2, file3], { configFilePath }); + + // If rootDir is not specified, root dir is where the config file is + const configRoot = path.dirname(configFilePath); + const replaceExtension = (f: string) => f.replace(/\.ts$/, ".lua"); + expect(getEmitPath(file1, program)).toBe(replaceExtension(path.join(configRoot, file1))); + expect(getEmitPath(file2, program)).toBe(replaceExtension(path.join(configRoot, file2))); + expect(getEmitPath(file3, program)).toBe(replaceExtension(path.join(configRoot, file3))); + }); +}); + +function normalize(path: string) { + return path.endsWith("/") ? path.slice(0, path.length - 1) : path; +} diff --git a/test/transpile/plugins/__snapshots__/plugins.spec.ts.snap b/test/transpile/plugins/__snapshots__/plugins.spec.ts.snap new file mode 100644 index 000000000..7e3be754d --- /dev/null +++ b/test/transpile/plugins/__snapshots__/plugins.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`statement comments 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + --This comment on variable declaration was added by a plugin! + --- This one too! + local foo = "bar" + -- This trailing comment was also added by a plugin! + --- Example luadoc comment + ---@param paramName ParamClass + foo = "baz" + --[[ This plugin can also (kinda) create multiline comments. + -- Line 2 + --]] + --[[Multiline comments are supported as arrays + This is the second line!]] + while true do + end + --Single line comments and multiline comments can also be mixed + --[[Like this + Pretty cool!]] + --Empty multiline comment below: + --[[]] + --Single line multiline comment: + --[[Single line]] +end +return ____exports" +`; diff --git a/test/transpile/plugins/add-comments.ts b/test/transpile/plugins/add-comments.ts new file mode 100644 index 000000000..2e9c56a5f --- /dev/null +++ b/test/transpile/plugins/add-comments.ts @@ -0,0 +1,61 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + visitors: { + [ts.SyntaxKind.VariableStatement](node, context) { + const result = context.superTransformStatements(node); + + const firstLuaStatement = result[0]; + const lastLuaStatement = result[result.length - 1]; + + firstLuaStatement.leadingComments = [ + "This comment on variable declaration was added by a plugin!", + "- This one too!", + ]; + + lastLuaStatement.trailingComments = [" This trailing comment was also added by a plugin!"]; + + return result; + }, + [ts.SyntaxKind.ExpressionStatement](node, context) { + const result = context.superTransformStatements(node); + + const firstLuaStatement = result[0]; + const lastLuaStatement = result[result.length - 1]; + + firstLuaStatement.leadingComments = ["- Example luadoc comment", "-@param paramName ParamClass"]; + + lastLuaStatement.trailingComments = [ + "[[ This plugin can also (kinda) create multiline comments.", + " Line 2", + "]]", + ]; + + return result; + }, + [ts.SyntaxKind.WhileStatement](node, context) { + const result = context.superTransformStatements(node); + + const firstLuaStatement = result[0]; + const lastLuaStatement = result[result.length - 1]; + + firstLuaStatement.leadingComments = [ + ["Multiline comments are supported as arrays", "This is the second line!"], + ]; + + lastLuaStatement.trailingComments = [ + "Single line comments and multiline comments can also be mixed", + ["Like this", "Pretty cool!"], + "Empty multiline comment below:", + [], + "Single line multiline comment:", + ["Single line"], + ]; + + return result; + }, + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/afterEmit.ts b/test/transpile/plugins/afterEmit.ts new file mode 100644 index 000000000..277258f25 --- /dev/null +++ b/test/transpile/plugins/afterEmit.ts @@ -0,0 +1,23 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + afterEmit(program: ts.Program, options: tstl.CompilerOptions, emitHost: tstl.EmitHost, result: tstl.EmitFile[]) { + void program; + void options; + void emitHost; + void result; + + const diagnostic = { + category: ts.DiagnosticCategory.Message, + messageText: "After emit diagnostic message!", + code: 1234, + file: program.getSourceFiles()[0], + start: undefined, + length: undefined, + } satisfies ts.Diagnostic; + return [diagnostic]; + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/afterPrint.ts b/test/transpile/plugins/afterPrint.ts new file mode 100644 index 000000000..cb548218b --- /dev/null +++ b/test/transpile/plugins/afterPrint.ts @@ -0,0 +1,21 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + afterPrint( + program: ts.Program, + options: tstl.CompilerOptions, + emitHost: tstl.EmitHost, + result: tstl.ProcessedFile[] + ) { + void program; + void options; + void emitHost; + + for (const file of result) { + file.code = "-- Comment added by afterPrint plugin\n" + file.code; + } + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/arguments.ts b/test/transpile/plugins/arguments.ts new file mode 100644 index 000000000..e90afae3b --- /dev/null +++ b/test/transpile/plugins/arguments.ts @@ -0,0 +1,27 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +interface Options { + name: string; + option: boolean; +} + +export default function plugin(options: Options): tstl.Plugin { + return { + visitors: { + [ts.SyntaxKind.ReturnStatement]: () => + tstl.createReturnStatement([ + tstl.createTableExpression([ + tstl.createTableFieldExpression( + tstl.createStringLiteral(options.name), + tstl.createStringLiteral("name") + ), + tstl.createTableFieldExpression( + tstl.createBooleanLiteral(options.option), + tstl.createStringLiteral("option") + ), + ]), + ]), + }, + }; +} diff --git a/test/transpile/plugins/beforeEmit.ts b/test/transpile/plugins/beforeEmit.ts new file mode 100644 index 000000000..2deb029e4 --- /dev/null +++ b/test/transpile/plugins/beforeEmit.ts @@ -0,0 +1,16 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + beforeEmit(program: ts.Program, options: tstl.CompilerOptions, emitHost: tstl.EmitHost, result: tstl.EmitFile[]) { + void program; + void options; + void emitHost; + + for (const file of result) { + file.code = "-- Comment added by beforeEmit plugin\n" + file.code; + } + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/beforeTransform.ts b/test/transpile/plugins/beforeTransform.ts new file mode 100644 index 000000000..c93c20f91 --- /dev/null +++ b/test/transpile/plugins/beforeTransform.ts @@ -0,0 +1,14 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + beforeTransform(program: ts.Program, options: tstl.CompilerOptions, emitHost: tstl.EmitHost) { + void program; + void emitHost; + + // Modify settings + options.outDir = "plugin/beforeTransform/outdir"; + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/moduleResolution.ts b/test/transpile/plugins/moduleResolution.ts new file mode 100644 index 000000000..705345a5b --- /dev/null +++ b/test/transpile/plugins/moduleResolution.ts @@ -0,0 +1,22 @@ +import path = require("path"); +import type * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + moduleResolution(moduleIdentifier) { + if (moduleIdentifier.includes("absolutefoo")) { + return path.join( + path.dirname(__dirname), + "module-resolution", + "project-with-module-resolution-plugin", + "lua_sources", + "absolutebar.lua" + ); + } + + if (moduleIdentifier.includes("foo")) { + return moduleIdentifier.replace("foo", "bar"); + } + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/plugins.spec.ts b/test/transpile/plugins/plugins.spec.ts new file mode 100644 index 000000000..ed25ae748 --- /dev/null +++ b/test/transpile/plugins/plugins.spec.ts @@ -0,0 +1,243 @@ +import * as path from "path"; +import * as util from "../../util"; +import { Plugin } from "../../../src/transpilation/plugins"; +import * as ts from "typescript"; + +test("printer", () => { + util.testModule`` + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "printer.ts") }] }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).toMatch("-- Custom printer plugin:")); +}); + +test("printer in bundle", () => { + const { transpiledFiles } = util.testBundle` + import "./otherfile"; + + const foo = "foo"; + ` + .addExtraFile( + "otherfile.ts", + ` + const bar = "bar"; + ` + ) + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "printer.ts") }] }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + expect(transpiledFiles).toHaveLength(1); + const lua = transpiledFiles[0].lua!; + + expect(lua).toContain("-- Custom printer plugin: main.lua"); + expect(lua).toContain("-- Custom printer plugin: otherfile.lua"); +}); + +test("visitor", () => { + util.testFunction` + return false; + ` + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "visitor.ts") }] }) + .expectToEqual(true); +}); + +test("visitor using super", () => { + util.testFunction` + return "foo"; + ` + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "visitor-super.ts") }] }) + .expectToEqual("bar"); +}); + +test("passing arguments", () => { + util.testFunction` + return {}; + ` + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "arguments.ts"), option: true }] }) + .expectToEqual({ name: path.join(__dirname, "arguments.ts"), option: true }); +}); + +test("statement comments", () => { + util.testFunction` + let foo = "bar"; + foo = "baz"; + while (true) {} + ` + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "add-comments.ts") }] }) + .expectLuaToMatchSnapshot(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1013 +test.each(["namespace", "module"])("%s with TS transformer plugin", moduleOrNamespace => { + util.testModule` + import { ns } from "module"; + export const result = ns.returnsBool(); + ` + .addExtraFile( + "module.ts", + ` + export ${moduleOrNamespace} ns { + export function returnsBool() { + return false; + } + } + ` + ) + .setOptions({ plugins: [{ transform: path.join(__dirname, "transformer-plugin.ts") }] }) + .expectNoExecutionError(); +}); + +test("beforeTransform plugin", () => { + const { transpiledFiles } = util.testModule` + console.log("Hello, World!"); + ` + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "beforeTransform.ts") }] }) + .getLuaResult(); + + expect(transpiledFiles).toHaveLength(1); + // Expect emitted to output path set by the plugin + expect(transpiledFiles[0].outPath).toContain(path.join("plugin", "beforeTransform", "outdir")); +}); + +test("afterPrint plugin", () => { + const { transpiledFiles } = util.testModule` + console.log("Hello, World!"); + ` + .addExtraFile( + "extrafile.ts", + ` + console.log("Hello, Mars!"); + ` + ) + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "afterPrint.ts") }] }) + .getLuaResult(); + + expect(transpiledFiles).toHaveLength(2); + for (const f of transpiledFiles) { + // Expect plugin inserted extra lua at start of file + expect(f.lua).toContain("-- Comment added by afterPrint plugin"); + } +}); + +test("beforeEmit plugin", () => { + const { transpiledFiles } = util.testModule` + console.log("Hello, World!"); + [].push(1,2,3); // Use lualib code + ` + .addExtraFile( + "extrafile.ts", + ` + console.log("Hello, Mars!"); + ` + ) + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "beforeEmit.ts") }] }) + .getLuaResult(); + + // 2 input files + 1 lualib_bundle + expect(transpiledFiles).toHaveLength(3); + expect(transpiledFiles.find(f => f.outPath.endsWith("lualib_bundle.lua"))).toBeDefined(); + for (const f of transpiledFiles) { + // Expect plugin inserted extra lua at start of all files including lualib bundle + expect(f.lua).toContain("-- Comment added by beforeEmit plugin"); + } +}); + +test("beforeEmit plugin bundle", () => { + const { transpiledFiles } = util.testBundle` + console.log("Hello, World!"); + [].push(1,2,3); // Use lualib code + ` + .addExtraFile( + "extrafile.ts", + ` + console.log("Hello, Mars!"); + ` + ) + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "beforeEmit.ts") }] }) + .getLuaResult(); + + // 1 lua bundle output + expect(transpiledFiles).toHaveLength(1); + for (const f of transpiledFiles) { + // Expect bundle to be affected by plugin + expect(f.lua).toContain("-- Comment added by beforeEmit plugin"); + } +}); + +test("afterEmit plugin", () => { + const { diagnostics } = util.testModule` + console.log("Hello, World!"); + [].push(1,2,3); // Use lualib code + ` + .addExtraFile( + "extrafile.ts", + ` + console.log("Hello, Mars!"); + ` + ) + .setOptions({ luaPlugins: [{ name: path.join(__dirname, "afterEmit.ts") }] }) + .getLuaResult(); + + // Expect to see the diagnostic returned by the plugin in the output + const diagnostic = diagnostics.find(d => d.code === 1234); + expect(diagnostic).toBeDefined(); + 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/plugins/printer.ts b/test/transpile/plugins/printer.ts new file mode 100644 index 000000000..37b6d84f7 --- /dev/null +++ b/test/transpile/plugins/printer.ts @@ -0,0 +1,23 @@ +import { SourceNode } from "source-map"; +import * as tstl from "../../../src"; + +class CustomPrinter extends tstl.LuaPrinter { + /* Override printFile */ + protected printFile(file: tstl.File): SourceNode { + const originalResult = super.printFile(file); + // Add header comment at the top of the file + return this.createSourceNode(file, [`-- Custom printer plugin: ${this.luaFile}\n`, originalResult]); + } + + /* Override printBoolean */ + public printBooleanLiteral(expression: tstl.BooleanLiteral): SourceNode { + // Print any boolean as 'true' + return this.createSourceNode(expression, "true"); + } +} + +const plugin: tstl.Plugin = { + printer: (program, emitHost, fileName, file) => new CustomPrinter(emitHost, program, fileName).print(file), +}; + +export default plugin; diff --git a/test/transpile/plugins/transformer-plugin.ts b/test/transpile/plugins/transformer-plugin.ts new file mode 100644 index 000000000..8b338723c --- /dev/null +++ b/test/transpile/plugins/transformer-plugin.ts @@ -0,0 +1,15 @@ +/** + * This is a TS tranformer plugin that replaces any return statement to 'return true'. + */ +import * as ts from "typescript"; + +const replaceNode = (node: ts.Node) => { + if (ts.isReturnStatement(node)) { + return ts.factory.createReturnStatement(ts.factory.createTrue()); + } +}; +const createTransformer = () => (context: ts.TransformationContext) => { + const visit = (node: ts.Node): ts.Node => replaceNode(node) ?? ts.visitEachChild(node, visit, context); + return (file: ts.SourceFile) => ts.visitNode(file, visit); +}; +exports.default = createTransformer; diff --git a/test/transpile/plugins/visitor-super.ts b/test/transpile/plugins/visitor-super.ts new file mode 100644 index 000000000..3ece9458b --- /dev/null +++ b/test/transpile/plugins/visitor-super.ts @@ -0,0 +1,18 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + visitors: { + [ts.SyntaxKind.StringLiteral](node, context) { + const result = context.superTransformExpression(node); + + if (tstl.isStringLiteral(result)) { + result.value = "bar"; + } + + return result; + }, + }, +}; + +export default plugin; diff --git a/test/transpile/plugins/visitor.ts b/test/transpile/plugins/visitor.ts new file mode 100644 index 000000000..15da18ded --- /dev/null +++ b/test/transpile/plugins/visitor.ts @@ -0,0 +1,10 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; + +const plugin: tstl.Plugin = { + visitors: { + [ts.SyntaxKind.ReturnStatement]: () => tstl.createReturnStatement([tstl.createBooleanLiteral(true)]), + }, +}; + +export default plugin; diff --git a/test/transpile/project.spec.ts b/test/transpile/project.spec.ts new file mode 100644 index 000000000..49234975b --- /dev/null +++ b/test/transpile/project.spec.ts @@ -0,0 +1,35 @@ +import * as path from "path"; +import { normalizeSlashes } from "../../src/utils"; +import * as util from "../util"; + +test("should transpile", () => { + const projectDir = path.join(__dirname, "project"); + const { transpiledFiles } = util + .testProject(path.join(projectDir, "tsconfig.json")) + .setMainFileName(path.join(projectDir, "index.ts")) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + expect( + transpiledFiles.map(f => ({ filePath: path.relative(projectDir, f.outPath), lua: f.lua })) + ).toMatchSnapshot(); +}); + +test("should give verbose output", () => { + // Capture console logs + const consoleLogs: string[] = []; + const originalLog = console.log; + console.log = (...args: any[]) => { + consoleLogs.push(args.map(a => a.toString().replace(normalizeSlashes(process.cwd()), "")).join(",")); + }; + + const projectDir = path.join(__dirname, "project"); + util.testProject(path.join(projectDir, "tsconfig.json")) + .setMainFileName(path.join(projectDir, "index.ts")) + .setOptions({ tstlVerbose: true }) + .expectToHaveNoDiagnostics(); + + console.log = originalLog; + + expect(consoleLogs).toMatchSnapshot(); +}); diff --git a/test/transpile/project/api.d.ts b/test/transpile/project/api.d.ts new file mode 100644 index 000000000..3375e6830 --- /dev/null +++ b/test/transpile/project/api.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +declare function getAPIValue(): number; +declare function setAPIValue(n: number): void; diff --git a/test/transpile/project/index.ts b/test/transpile/project/index.ts new file mode 100644 index 000000000..a244bdbfb --- /dev/null +++ b/test/transpile/project/index.ts @@ -0,0 +1,4 @@ +import { getNumber } from "./otherFile"; + +const myNumber = getNumber(); +setAPIValue(myNumber * 5); diff --git a/test/transpile/project/otherFile.ts b/test/transpile/project/otherFile.ts new file mode 100644 index 000000000..0ee7f628f --- /dev/null +++ b/test/transpile/project/otherFile.ts @@ -0,0 +1,3 @@ +export function getNumber(): number { + return getAPIValue(); +} diff --git a/test/transpile/project/tsconfig.json b/test/transpile/project/tsconfig.json new file mode 100644 index 000000000..8fb9d9e90 --- /dev/null +++ b/test/transpile/project/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + } +} diff --git a/test/transpile/resolve-plugin/cjs.js b/test/transpile/resolve-plugin/cjs.js new file mode 100644 index 000000000..ec01c2c14 --- /dev/null +++ b/test/transpile/resolve-plugin/cjs.js @@ -0,0 +1 @@ +module.exports = true; diff --git a/test/transpile/resolve-plugin/import.ts b/test/transpile/resolve-plugin/import.ts new file mode 100644 index 000000000..3d831e6ad --- /dev/null +++ b/test/transpile/resolve-plugin/import.ts @@ -0,0 +1 @@ +export const named = true; diff --git a/test/transpile/resolve-plugin/resolve-plugin.spec.ts b/test/transpile/resolve-plugin/resolve-plugin.spec.ts new file mode 100644 index 000000000..7b48bad8f --- /dev/null +++ b/test/transpile/resolve-plugin/resolve-plugin.spec.ts @@ -0,0 +1,27 @@ +import * as path from "path"; +import { resolvePlugin } from "../../../src/transpilation/utils"; + +test("resolve relative module paths", () => { + const result = resolvePlugin("test", "test", __dirname, "./ts.ts"); + expect(result).toMatchObject({ result: true }); +}); + +test("load .ts modules", () => { + const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "ts.ts")); + expect(result).toMatchObject({ result: true }); +}); + +test("load CJS .js modules", () => { + const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "cjs.js")); + expect(result).toMatchObject({ result: true }); +}); + +test("load transpiled ESM .js modules", () => { + const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "transpiled-esm.js")); + expect(result).toMatchObject({ result: true }); +}); + +test('"import" option', () => { + const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "import.ts"), "named"); + expect(result).toMatchObject({ result: true }); +}); diff --git a/test/transpile/resolve-plugin/transpiled-esm.js b/test/transpile/resolve-plugin/transpiled-esm.js new file mode 100644 index 000000000..ebf774058 --- /dev/null +++ b/test/transpile/resolve-plugin/transpiled-esm.js @@ -0,0 +1,2 @@ +module.exports.__esModule = true; +module.exports.default = true; diff --git a/test/transpile/resolve-plugin/ts.ts b/test/transpile/resolve-plugin/ts.ts new file mode 100644 index 000000000..ff3177bab --- /dev/null +++ b/test/transpile/resolve-plugin/ts.ts @@ -0,0 +1 @@ +export default true; diff --git a/test/transpile/run.ts b/test/transpile/run.ts new file mode 100644 index 000000000..a24a76b9f --- /dev/null +++ b/test/transpile/run.ts @@ -0,0 +1,17 @@ +import * as path from "path"; +import * as ts from "typescript"; +import * as tstl from "../../src"; +import { normalizeSlashes } from "../../src/utils"; + +export function transpileFilesResult(rootNames: string[], options: tstl.CompilerOptions) { + options.skipLibCheck = true; + options.types = []; + + const emittedFiles: ts.OutputFile[] = []; + const { diagnostics } = tstl.transpileFiles(rootNames, options, (fileName, text, writeByteOrderMark) => { + const name = normalizeSlashes(path.relative(__dirname, fileName)); + emittedFiles.push({ name, text, writeByteOrderMark }); + }); + + return { diagnostics, emittedFiles }; +} diff --git a/test/transpile/transformers/fixtures.ts b/test/transpile/transformers/fixtures.ts new file mode 100644 index 000000000..dff7667be --- /dev/null +++ b/test/transpile/transformers/fixtures.ts @@ -0,0 +1,44 @@ +import * as assert from "assert"; +import * as ts from "typescript"; +import * as tstl from "../../../src"; +import { visitAndReplace } from "./utils"; + +export const program = (program: ts.Program, options: { value: any }): ts.TransformerFactory => + checker(program.getTypeChecker(), options); + +export const config = + ({ value }: { value: any }): ts.TransformerFactory => + context => + file => + visitAndReplace(context, file, node => { + if (!ts.isReturnStatement(node)) return; + return ts.factory.updateReturnStatement(node, value ? ts.factory.createTrue() : ts.factory.createFalse()); + }); + +export const checker = + (checker: ts.TypeChecker, { value }: { value: any }): ts.TransformerFactory => + context => + file => + visitAndReplace(context, file, node => { + if (!ts.isReturnStatement(node) || !node.expression) return; + const type = checker.getTypeAtLocation(node.expression); + if ((type.flags & ts.TypeFlags.BooleanLiteral) === 0) return; + return ts.factory.updateReturnStatement(node, value ? ts.factory.createTrue() : ts.factory.createFalse()); + }); + +export const raw: ts.TransformerFactory = context => file => + visitAndReplace(context, file, node => { + if (!ts.isReturnStatement(node)) return; + return ts.factory.updateReturnStatement(node, ts.factory.createTrue()); + }); + +export const compilerOptions = + (options: tstl.CompilerOptions): ts.TransformerFactory => + context => + file => { + assert(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/transpile/transformers/transformers.spec.ts b/test/transpile/transformers/transformers.spec.ts new file mode 100644 index 000000000..34dd9dc15 --- /dev/null +++ b/test/transpile/transformers/transformers.spec.ts @@ -0,0 +1,56 @@ +import * as path from "path"; +import * as util from "../../util"; + +test("ignore language service plugins", () => { + util.testFunction` + return false; + ` + .setOptions({ plugins: [{ name: path.join(__dirname, "types.ts") }] }) + .expectToEqual(false); +}); + +test("default type", () => { + util.testFunction` + return false; + ` + .setOptions({ plugins: [{ transform: path.join(__dirname, "fixtures.ts"), import: "program", value: true }] }) + .expectToEqual(true); +}); + +test("transformer resolution error", () => { + util.testModule`` + .setOptions({ plugins: [{ transform: path.join(__dirname, "error.ts") }] }) + .expectToHaveDiagnostics(); +}); + +// This tests plugin transformers by transforming the `return false` statement to a `return true` statement. +describe("factory types", () => { + test.each(["program", "config", "checker", "raw", "compilerOptions"] as const)("%s", type => { + util.testFunction` + return false; + ` + .setOptions({ + plugins: [{ transform: path.join(__dirname, "fixtures.ts"), type, import: type, value: true }], + }) + .expectToEqual(true); + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1464 +test("transformer with switch does not break (#1464)", () => { + util.testFunction` + const foo: number = 3; + switch (foo) { + case 2: { + return 10; + } + case 3: { + return false; + } + } + ` + .setOptions({ + plugins: [{ transform: path.join(__dirname, "fixtures.ts"), import: "program", value: true }], + }) + .expectToEqual(true); +}); diff --git a/test/transpile/transformers/utils.ts b/test/transpile/transformers/utils.ts new file mode 100644 index 000000000..819454530 --- /dev/null +++ b/test/transpile/transformers/utils.ts @@ -0,0 +1,6 @@ +import * as ts from "typescript"; + +export function visitAndReplace(context: ts.TransformationContext, node: T, visitor: ts.Visitor): T { + const visit: ts.Visitor = node => visitor(node) ?? ts.visitEachChild(node, visit, context); + return ts.visitEachChild(node, visit, context); +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 7eb5d7f53..86d7c9359 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,13 +1,18 @@ { + "extends": "../tsconfig.json", "compilerOptions": { - "experimentalDecorators": true, - "sourceMap": true, - "target": "es2017", - "module": "commonjs" + "rootDir": "..", + "types": ["node", "jest"], + "baseUrl": "." }, + "include": [".", "../src"], "exclude": [ - "translation/ts/*", - "compiler/projects/*", - "compiler/testfiles/*" + "translation/transformation", + "cli/errors", + "cli/watch", + "transpile/directories", + "transpile/outFile", + "../src/lualib", + "../language-extensions" ] } diff --git a/test/unit/__snapshots__/assignments.spec.ts.snap b/test/unit/__snapshots__/assignments.spec.ts.snap new file mode 100644 index 000000000..f28bc871c --- /dev/null +++ b/test/unit/__snapshots__/assignments.spec.ts.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`var is disallowed for loop: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + do + local foo = 0 + while true do + end + end +end +return ____exports" +`; + +exports[`var is disallowed for loop: diagnostics 1`] = `"main.ts(2,18): error TSTL: \`var\` declarations are not supported. Use \`let\` or \`const\` instead."`; + +exports[`var is disallowed for...in loop: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + for foo in pairs({}) do + end +end +return ____exports" +`; + +exports[`var is disallowed for...in loop: diagnostics 1`] = `"main.ts(2,18): error TSTL: \`var\` declarations are not supported. Use \`let\` or \`const\` instead."`; + +exports[`var is disallowed for...of loop: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + for ____, foo in ipairs({}) do + end +end +return ____exports" +`; + +exports[`var is disallowed for...of loop: diagnostics 1`] = `"main.ts(2,18): error TSTL: \`var\` declarations are not supported. Use \`let\` or \`const\` instead."`; + +exports[`var is disallowed var declaration: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local foo = true +end +return ____exports" +`; + +exports[`var is disallowed var declaration: diagnostics 1`] = `"main.ts(2,13): error TSTL: \`var\` declarations are not supported. Use \`let\` or \`const\` instead."`; diff --git a/test/unit/__snapshots__/bundle.spec.ts.snap b/test/unit/__snapshots__/bundle.spec.ts.snap new file mode 100644 index 000000000..852b6a668 --- /dev/null +++ b/test/unit/__snapshots__/bundle.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LuaLibImportKind.Inline generates a warning: diagnostics 1`] = `"warning TSTL: Using 'luaBundle' with 'luaLibImport: "inline"' might generate duplicate code. It is recommended to use 'luaLibImport: "require"'."`; + +exports[`bundling not allowed for buildmode library: diagnostics 1`] = `"error TSTL: Cannot bundle projects with "buildmode": "library". Projects including the library can still bundle (which will include external library files)."`; + +exports[`luaEntry doesn't exist: diagnostics 1`] = `"error TSTL: Could not find bundle entry point 'entry.ts'. It should be a file in the project."`; + +exports[`no entry point: diagnostics 1`] = `"error TSTL: 'luaBundleEntry' is required when 'luaBundle' is enabled."`; diff --git a/test/unit/__snapshots__/comments.spec.ts.snap b/test/unit/__snapshots__/comments.spec.ts.snap new file mode 100644 index 000000000..55052f60e --- /dev/null +++ b/test/unit/__snapshots__/comments.spec.ts.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSDoc is copied on a function with tags: code 1`] = ` +"--- This is a function comment. +-- It has multiple lines. +-- +-- @param arg1 This is the first argument. +-- @param arg2 This is the second argument. +-- @returns A very powerful string. +function foo(self, arg1, arg2) + return "bar" +end" +`; + +exports[`JSDoc is copied on a function with tags: diagnostics 1`] = `""`; + +exports[`JSDoc is copied on a variable: code 1`] = ` +"--- This is a variable comment. +foo = 123" +`; + +exports[`JSDoc is copied on a variable: diagnostics 1`] = `""`; + +exports[`Multi-line JSDoc with one block is copied on a function: code 1`] = ` +"--- This is a function comment. +-- It has more than one line. +function foo(self) +end" +`; + +exports[`Multi-line JSDoc with one block is copied on a function: diagnostics 1`] = `""`; + +exports[`Multi-line JSDoc with two blocks is copied on a function: code 1`] = ` +"--- This is a function comment. +-- It has more than one line. +-- +-- It also has more than one block. +function foo(self) +end" +`; + +exports[`Multi-line JSDoc with two blocks is copied on a function: diagnostics 1`] = `""`; + +exports[`Single-line JSDoc is copied on a function: code 1`] = ` +"--- This is a function comment. +function foo(self) +end" +`; + +exports[`Single-line JSDoc is copied on a function: diagnostics 1`] = `""`; 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 new file mode 100644 index 000000000..e2adff370 --- /dev/null +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -0,0 +1,784 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Binary expressions ordering parentheses ("-1+1") 1`] = ` +"local ____exports = {} +____exports.__result = -1 + 1 +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1*(3+4)") 1`] = ` +"local ____exports = {} +____exports.__result = 1 * (3 + 4) +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1*(3+4*2)") 1`] = ` +"local ____exports = {} +____exports.__result = 1 * (3 + 4 * 2) +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1*30+4") 1`] = ` +"local ____exports = {} +____exports.__result = 1 * 30 + 4 +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1+1") 1`] = ` +"local ____exports = {} +____exports.__result = 1 + 1 +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("10-(4+5)") 1`] = ` +"local ____exports = {} +____exports.__result = 10 - (4 + 5) +return ____exports" +`; + +exports[`Bitop [5.0] ("~a"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.bnot(a) +return ____exports" +`; + +exports[`Bitop [5.0] ("~a"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a&=b"): code 1`] = ` +"local ____exports = {} +a = bit.band(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.0] ("a&=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a&b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.band(a, b) +return ____exports" +`; + +exports[`Bitop [5.0] ("a&b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a<<=b"): code 1`] = ` +"local ____exports = {} +a = bit.lshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.0] ("a<<=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a<>=b"): code 1`] = ` +"local ____exports = {} +a = bit.arshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.0] ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a>>>=b"): code 1`] = ` +"local ____exports = {} +a = bit.rshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.0] ("a>>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a>>>b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.rshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.0] ("a>>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a>>b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.arshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.0] ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a^=b"): code 1`] = ` +"local ____exports = {} +a = bit.bxor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.0] ("a^=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a^b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.bxor(a, b) +return ____exports" +`; + +exports[`Bitop [5.0] ("a^b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a|=b"): code 1`] = ` +"local ____exports = {} +a = bit.bor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.0] ("a|=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.0] ("a|b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.bor(a, b) +return ____exports" +`; + +exports[`Bitop [5.0] ("a|b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`Bitop [5.1] ("~a"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.bnot(a) +return ____exports" +`; + +exports[`Bitop [5.1] ("~a"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a&=b"): code 1`] = ` +"local ____exports = {} +a = bit.band(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.1] ("a&=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a&b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.band(a, b) +return ____exports" +`; + +exports[`Bitop [5.1] ("a&b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a<<=b"): code 1`] = ` +"local ____exports = {} +a = bit.lshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.1] ("a<<=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a<>=b"): code 1`] = ` +"local ____exports = {} +a = bit.arshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.1] ("a>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a>>>=b"): code 1`] = ` +"local ____exports = {} +a = bit.rshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.1] ("a>>>=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a>>>b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.rshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.1] ("a>>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a>>b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.arshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.1] ("a>>b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a^=b"): code 1`] = ` +"local ____exports = {} +a = bit.bxor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.1] ("a^=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a^b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.bxor(a, b) +return ____exports" +`; + +exports[`Bitop [5.1] ("a^b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a|=b"): code 1`] = ` +"local ____exports = {} +a = bit.bor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.1] ("a|=b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.1] ("a|b"): code 1`] = ` +"local ____exports = {} +____exports.__result = bit.bor(a, b) +return ____exports" +`; + +exports[`Bitop [5.1] ("a|b"): diagnostics 1`] = `"main.ts(1,25): error TSTL: Bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`Bitop [5.2] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.bnot(a) +return ____exports" +`; + +exports[`Bitop [5.2] ("a&=b") 1`] = ` +"local ____exports = {} +a = bit32.band(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.2] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.band(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a<<=b") 1`] = ` +"local ____exports = {} +a = bit32.lshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.2] ("a<>=b") 1`] = ` +"local ____exports = {} +a = bit32.arshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.2] ("a>>>=b") 1`] = ` +"local ____exports = {} +a = bit32.rshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.2] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.rshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.arshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a^=b") 1`] = ` +"local ____exports = {} +a = bit32.bxor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.2] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.bxor(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a|=b") 1`] = ` +"local ____exports = {} +a = bit32.bor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.2] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.bor(a, b) +return ____exports" +`; + +exports[`Bitop [5.3] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = ~a +return ____exports" +`; + +exports[`Bitop [5.3] ("a&=b") 1`] = ` +"local ____exports = {} +a = a & b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.3] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = a & b +return ____exports" +`; + +exports[`Bitop [5.3] ("a<<=b") 1`] = ` +"local ____exports = {} +a = a << b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.3] ("a<>>=b") 1`] = ` +"local ____exports = {} +a = a >> b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.3] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = a >> b +return ____exports" +`; + +exports[`Bitop [5.3] ("a^=b") 1`] = ` +"local ____exports = {} +a = a ~ b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.3] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = a ~ b +return ____exports" +`; + +exports[`Bitop [5.3] ("a|=b") 1`] = ` +"local ____exports = {} +a = a | b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.3] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = a | b +return ____exports" +`; + +exports[`Bitop [5.4] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = ~a +return ____exports" +`; + +exports[`Bitop [5.4] ("a&=b") 1`] = ` +"local ____exports = {} +a = a & b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.4] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = a & b +return ____exports" +`; + +exports[`Bitop [5.4] ("a<<=b") 1`] = ` +"local ____exports = {} +a = a << b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.4] ("a<>>=b") 1`] = ` +"local ____exports = {} +a = a >> b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.4] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = a >> b +return ____exports" +`; + +exports[`Bitop [5.4] ("a^=b") 1`] = ` +"local ____exports = {} +a = a ~ b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.4] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = a ~ b +return ____exports" +`; + +exports[`Bitop [5.4] ("a|=b") 1`] = ` +"local ____exports = {} +a = a | b +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [5.4] ("a|b") 1`] = ` +"local ____exports = {} +____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 >> 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 [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) +return ____exports" +`; + +exports[`Bitop [JIT] ("a&=b") 1`] = ` +"local ____exports = {} +a = bit.band(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [JIT] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.band(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a<<=b") 1`] = ` +"local ____exports = {} +a = bit.lshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [JIT] ("a<>=b") 1`] = ` +"local ____exports = {} +a = bit.arshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [JIT] ("a>>>=b") 1`] = ` +"local ____exports = {} +a = bit.rshift(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [JIT] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.rshift(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.arshift(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a^=b") 1`] = ` +"local ____exports = {} +a = bit.bxor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [JIT] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.bxor(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a|=b") 1`] = ` +"local ____exports = {} +a = bit.bor(a, b) +____exports.__result = a +return ____exports" +`; + +exports[`Bitop [JIT] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.bor(a, b) +return ____exports" +`; + +exports[`Null Expression 1`] = ` +"local ____exports = {} +____exports.__result = nil +return ____exports" +`; + +exports[`Unary expressions basic ("!a") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = not a +end +return ____exports" +`; + +exports[`Unary expressions basic ("++i") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i + 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("+a") 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Number = ____lualib.__TS__Number +local ____exports = {} +function ____exports.__main(self) + __TS__Number(a) +end +return ____exports" +`; + +exports[`Unary expressions basic ("--i") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i - 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("-a") 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Number = ____lualib.__TS__Number +local ____exports = {} +function ____exports.__main(self) + __TS__Number(-a) +end +return ____exports" +`; + +exports[`Unary expressions basic ("delete tbl.test") 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Delete = ____lualib.__TS__Delete +local ____exports = {} +function ____exports.__main(self) + __TS__Delete(tbl, "test") +end +return ____exports" +`; + +exports[`Unary expressions basic ("delete tbl['test']") 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Delete = ____lualib.__TS__Delete +local ____exports = {} +function ____exports.__main(self) + __TS__Delete(tbl, "test") +end +return ____exports" +`; + +exports[`Unary expressions basic ("i++") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i + 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("i--") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i - 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("let a = delete tbl.test") 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Delete = ____lualib.__TS__Delete +local ____exports = {} +function ____exports.__main(self) + local a = __TS__Delete(tbl, "test") +end +return ____exports" +`; + +exports[`Unary expressions basic ("let a = delete tbl['test']") 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Delete = ____lualib.__TS__Delete +local ____exports = {} +function ____exports.__main(self) + local a = __TS__Delete(tbl, "test") +end +return ____exports" +`; + +exports[`Undefined Expression 1`] = ` +"local ____exports = {} +____exports.__result = nil +return ____exports" +`; + +exports[`Unsupported bitop 5.3 ("a>>=b"): code 1`] = ` +"local ____exports = {} +a = a >> b +____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"): code 1`] = ` +"local ____exports = {} +____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.4 ("a>>=b"): code 1`] = ` +"local ____exports = {} +a = a >> b +____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"): code 1`] = ` +"local ____exports = {} +____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."`; diff --git a/test/unit/__snapshots__/file.spec.ts.snap b/test/unit/__snapshots__/file.spec.ts.snap new file mode 100644 index 000000000..12a004c6c --- /dev/null +++ b/test/unit/__snapshots__/file.spec.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shebang CRLF 1`] = ` +"#!/usr/bin/env lua +foo = true" +`; + +exports[`shebang LF 1`] = ` +"#!/usr/bin/env lua +foo = true" +`; diff --git a/test/unit/__snapshots__/identifiers.spec.ts.snap b/test/unit/__snapshots__/identifiers.spec.ts.snap new file mode 100644 index 000000000..b90b10398 --- /dev/null +++ b/test/unit/__snapshots__/identifiers.spec.ts.snap @@ -0,0 +1,444 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ambient identifier cannot be a lua keyword ("class local {}"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("class local {}"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("const foo: any, bar: any, local: any;"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("const foo: any, bar: any, local: any;"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("const local: any;"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("const local: any;"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("enum local {}"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("enum local {}"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("function local() {}"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("function local() {}"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("let local: any;"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("let local: any;"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("module local { export const bar: any; }"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("module local { export const bar: any; }"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("namespace local { export const bar: any; }"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("namespace local { export const bar: any; }"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier cannot be a lua keyword ("var local: any;"): code 1`] = `"local ____ = ____local"`; + +exports[`ambient identifier cannot be a lua keyword ("var local: any;"): diagnostics 1`] = `"main.ts(3,9): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier ("class $$ {}"): code 1`] = `"local ____ = _____24_24_24"`; + +exports[`ambient identifier must be a valid lua identifier ("class $$ {}"): 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 ("const $$: any;"): code 1`] = `"local ____ = _____24_24_24"`; + +exports[`ambient identifier must be a valid lua identifier ("const $$: 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 ("const foo: any, bar: any, $$: any;"): code 1`] = `"local ____ = _____24_24_24"`; + +exports[`ambient identifier must be a valid lua identifier ("const foo: any, bar: any, $$: 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 ("enum $$ {}"): code 1`] = `"local ____ = _____24_24_24"`; + +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 $$();"): 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."`; + +exports[`ambient identifier must be a valid lua identifier ("var $$: any;"): code 1`] = `"local ____ = _____24_24_24"`; + +exports[`ambient identifier must be a valid lua identifier ("var $$: 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 (object literal shorthand) ("$$"): code 1`] = `"foo = {["$$$"] = _____24_24_24}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("$$"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("_̀ः٠‿"): code 1`] = `"foo = {["_̀ः٠‿"] = ______300_903_660_203F}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("_̀ः٠‿"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name '_̀ः٠‿'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("and"): code 1`] = `"foo = {["and"] = ____and}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("and"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'and'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("elseif"): code 1`] = `"foo = {["elseif"] = ____elseif}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("elseif"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'elseif'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("end"): code 1`] = `"foo = {["end"] = ____end}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("end"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'end'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("goto"): code 1`] = `"foo = {["goto"] = ____goto}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("goto"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'goto'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("local"): code 1`] = `"foo = {["local"] = ____local}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("local"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("nil"): code 1`] = `"foo = {["nil"] = ____nil}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("nil"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'nil'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("not"): code 1`] = `"foo = {["not"] = ____not}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("not"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'not'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("or"): code 1`] = `"foo = {["or"] = ____or}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("or"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'or'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("repeat"): code 1`] = `"foo = {["repeat"] = ____repeat}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("repeat"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'repeat'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("then"): code 1`] = `"foo = {["then"] = ____then}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("then"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'then'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("until"): code 1`] = `"foo = {["until"] = ____until}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("until"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'until'. Ambient identifiers must be valid lua identifiers."`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("ɥɣɎɌͼƛಠ"): code 1`] = `"foo = {["ɥɣɎɌͼƛಠ"] = _____265_263_24E_24C_37C_19B_CA0}"`; + +exports[`ambient identifier must be a valid lua identifier (object literal shorthand) ("ɥɣɎɌͼƛಠ"): diagnostics 1`] = `"main.ts(3,27): error TSTL: Invalid ambient identifier name 'ɥɣɎɌͼƛಠ'. Ambient identifiers must be valid lua identifiers."`; + +exports[`declaration-only variable with lua keyword as name is not renamed 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + type(7) +end +return ____exports" +`; + +exports[`undeclared identifier must be a valid lua identifier ("$$"): code 1`] = `"foo = _____24_24_24"`; + +exports[`undeclared identifier must be a valid lua identifier ("$$"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("_̀ः٠‿"): code 1`] = `"foo = ______300_903_660_203F"`; + +exports[`undeclared identifier must be a valid lua identifier ("_̀ः٠‿"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name '_̀ः٠‿'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("and"): code 1`] = `"foo = ____and"`; + +exports[`undeclared identifier must be a valid lua identifier ("and"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'and'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("elseif"): code 1`] = `"foo = ____elseif"`; + +exports[`undeclared identifier must be a valid lua identifier ("elseif"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'elseif'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("end"): code 1`] = `"foo = ____end"`; + +exports[`undeclared identifier must be a valid lua identifier ("end"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'end'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("goto"): code 1`] = `"foo = ____goto"`; + +exports[`undeclared identifier must be a valid lua identifier ("goto"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'goto'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("local"): code 1`] = `"foo = ____local"`; + +exports[`undeclared identifier must be a valid lua identifier ("local"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("nil"): code 1`] = `"foo = ____nil"`; + +exports[`undeclared identifier must be a valid lua identifier ("nil"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'nil'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("not"): code 1`] = `"foo = ____not"`; + +exports[`undeclared identifier must be a valid lua identifier ("not"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'not'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("or"): code 1`] = `"foo = ____or"`; + +exports[`undeclared identifier must be a valid lua identifier ("or"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'or'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("repeat"): code 1`] = `"foo = ____repeat"`; + +exports[`undeclared identifier must be a valid lua identifier ("repeat"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'repeat'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("then"): code 1`] = `"foo = ____then"`; + +exports[`undeclared identifier must be a valid lua identifier ("then"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'then'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("until"): code 1`] = `"foo = ____until"`; + +exports[`undeclared identifier must be a valid lua identifier ("until"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'until'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier ("ɥɣɎɌͼƛಠ"): code 1`] = `"foo = _____265_263_24E_24C_37C_19B_CA0"`; + +exports[`undeclared identifier must be a valid lua identifier ("ɥɣɎɌͼƛಠ"): diagnostics 1`] = `"main.ts(2,21): error TSTL: Invalid ambient identifier name 'ɥɣɎɌͼƛಠ'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("$$"): code 1`] = `"foo = {["$$$"] = _____24_24_24}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("$$"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name '$$$'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("_̀ः٠‿"): code 1`] = `"foo = {["_̀ः٠‿"] = ______300_903_660_203F}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("_̀ः٠‿"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name '_̀ः٠‿'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("and"): code 1`] = `"foo = {["and"] = ____and}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("and"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'and'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("elseif"): code 1`] = `"foo = {["elseif"] = ____elseif}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("elseif"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'elseif'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("end"): code 1`] = `"foo = {["end"] = ____end}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("end"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'end'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("goto"): code 1`] = `"foo = {["goto"] = ____goto}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("goto"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'goto'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("local"): code 1`] = `"foo = {["local"] = ____local}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("local"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'local'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("nil"): code 1`] = `"foo = {["nil"] = ____nil}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("nil"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'nil'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("not"): code 1`] = `"foo = {["not"] = ____not}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("not"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'not'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("or"): code 1`] = `"foo = {["or"] = ____or}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("or"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'or'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("repeat"): code 1`] = `"foo = {["repeat"] = ____repeat}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("repeat"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'repeat'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("then"): code 1`] = `"foo = {["then"] = ____then}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("then"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'then'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("until"): code 1`] = `"foo = {["until"] = ____until}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("until"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'until'. Ambient identifiers must be valid lua identifiers."`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("ɥɣɎɌͼƛಠ"): code 1`] = `"foo = {["ɥɣɎɌͼƛಠ"] = _____265_263_24E_24C_37C_19B_CA0}"`; + +exports[`undeclared identifier must be a valid lua identifier (object literal shorthand) ("ɥɣɎɌͼƛಠ"): diagnostics 1`] = `"main.ts(2,27): error TSTL: Invalid ambient identifier name 'ɥɣɎɌͼƛಠ'. Ambient identifiers must be valid lua identifiers."`; + +exports[`unicode identifiers in supporting environments (luajit) destructuring property name ("_̀ः٠‿") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____temp_0 = {foo = "foobar"} + local _̀ः٠‿ = ____temp_0.foo + return _̀ः٠‿ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) destructuring property name ("ɥɣɎɌͼƛಠ") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____temp_0 = {foo = "foobar"} + local ɥɣɎɌͼƛಠ = ____temp_0.foo + return ɥɣɎɌͼƛಠ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) destructuring property name ("𝛼𝛽𝚫") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____temp_0 = {foo = "foobar"} + local 𝛼𝛽𝚫 = ____temp_0.foo + return 𝛼𝛽𝚫 +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) destructuring shorthand ("_̀ः٠‿") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____temp_0 = {_̀ः٠‿ = "foobar"} + local _̀ः٠‿ = ____temp_0._̀ः٠‿ + return _̀ः٠‿ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) destructuring shorthand ("ɥɣɎɌͼƛಠ") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____temp_0 = {ɥɣɎɌͼƛಠ = "foobar"} + local ɥɣɎɌͼƛಠ = ____temp_0.ɥɣɎɌͼƛಠ + return ɥɣɎɌͼƛಠ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) destructuring shorthand ("𝛼𝛽𝚫") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____temp_0 = {𝛼𝛽𝚫 = "foobar"} + local 𝛼𝛽𝚫 = ____temp_0.𝛼𝛽𝚫 + return 𝛼𝛽𝚫 +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) function ("_̀ः٠‿") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function _̀ः٠‿(self, arg) + return "foo" .. arg + end + return _̀ः٠‿(nil, "bar") +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) function ("ɥɣɎɌͼƛಠ") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function ɥɣɎɌͼƛಠ(self, arg) + return "foo" .. arg + end + return ɥɣɎɌͼƛಠ(nil, "bar") +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) function ("𝛼𝛽𝚫") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function 𝛼𝛽𝚫(self, arg) + return "foo" .. arg + end + return 𝛼𝛽𝚫(nil, "bar") +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) identifier name ("_̀ः٠‿") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local _̀ः٠‿ = "foobar" + return _̀ः٠‿ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) identifier name ("ɥɣɎɌͼƛಠ") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ɥɣɎɌͼƛಠ = "foobar" + return ɥɣɎɌͼƛಠ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) identifier name ("𝛼𝛽𝚫") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local 𝛼𝛽𝚫 = "foobar" + return 𝛼𝛽𝚫 +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) method ("_̀ः٠‿") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local foo = {_̀ः٠‿ = function(self, arg) + return "foo" .. arg + end} + return foo:_̀ः٠‿("bar") +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) method ("ɥɣɎɌͼƛಠ") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local foo = {ɥɣɎɌͼƛಠ = function(self, arg) + return "foo" .. arg + end} + return foo:ɥɣɎɌͼƛಠ("bar") +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) method ("𝛼𝛽𝚫") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local foo = {𝛼𝛽𝚫 = function(self, arg) + return "foo" .. arg + end} + return foo:𝛼𝛽𝚫("bar") +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) property name ("_̀ः٠‿") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = {_̀ः٠‿ = "foobar"} + return x._̀ः٠‿ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) property name ("ɥɣɎɌͼƛಠ") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = {ɥɣɎɌͼƛಠ = "foobar"} + return x.ɥɣɎɌͼƛಠ +end +return ____exports" +`; + +exports[`unicode identifiers in supporting environments (luajit) property name ("𝛼𝛽𝚫") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = {𝛼𝛽𝚫 = "foobar"} + return x.𝛼𝛽𝚫 +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 new file mode 100644 index 000000000..29b609730 --- /dev/null +++ b/test/unit/__snapshots__/loops.spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`forin[Array]: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local array = {} + for key in pairs(array) do + end +end +return ____exports" +`; + +exports[`forin[Array]: diagnostics 1`] = `"main.ts(3,9): error TSTL: Iterating over arrays with 'for ... in' is not allowed."`; diff --git a/test/unit/__snapshots__/optionalChaining.spec.ts.snap b/test/unit/__snapshots__/optionalChaining.spec.ts.snap new file mode 100644 index 000000000..19fa2a309 --- /dev/null +++ b/test/unit/__snapshots__/optionalChaining.spec.ts.snap @@ -0,0 +1,276 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Unsupported optional chains Builtin global method: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Number = ____lualib.__TS__Number +local ____opt_0 = Number +if ____opt_0 ~= nil then + __TS__Number("3") +end" +`; + +exports[`Unsupported optional chains Builtin global method: diagnostics 1`] = `"main.ts(2,17): error TSTL: Optional calls are not supported for builtin or language extension functions."`; + +exports[`Unsupported optional chains Builtin global property: code 1`] = ` +"local ____opt_0 = console +if ____opt_0 ~= nil then + print("3") +end" +`; + +exports[`Unsupported optional chains Builtin global property: diagnostics 1`] = `"main.ts(2,17): error TSTL: Optional calls are not supported for builtin or language extension functions."`; + +exports[`Unsupported optional chains Builtin prototype method: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__ArrayForEach = ____lualib.__TS__ArrayForEach +local ____opt_0 = ({1, 2, 3, 4}).forEach +if ____opt_0 ~= nil then + __TS__ArrayForEach( + {1, 2, 3, 4}, + function() + end + ) +end" +`; + +exports[`Unsupported optional chains Builtin prototype method: diagnostics 1`] = `"main.ts(2,17): error TSTL: Optional calls are not supported for builtin or language extension functions."`; + +exports[`Unsupported optional chains Compile members only: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + --- + -- @compileMembersOnly + local A = 0 + --- + -- @compileMembersOnly + local B = 2 + --- + -- @compileMembersOnly + local C = 3 + --- + -- @compileMembersOnly + local D = "D" + local ____opt_0 = TestEnum + if ____opt_0 ~= nil then + local ____ = B + end +end +return ____exports" +`; + +exports[`Unsupported optional chains Compile members only: diagnostics 1`] = `"main.ts(10,17): error TSTL: Optional calls are not supported on enums marked with @compileMembersOnly."`; + +exports[`Unsupported optional chains Language extensions: code 1`] = ` +"local ____opt_0 = ({}).has +if ____opt_0 ~= nil then +end" +`; + +exports[`Unsupported optional chains Language extensions: diagnostics 1`] = ` +"main.ts(2,17): error TSTL: Optional calls are not supported for builtin or language extension functions. +main.ts(2,17): error TSTL: This language extension must be called as a method." +`; + +exports[`long optional chain 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local a = {b = {c = {d = {e = {f = "hello!"}}}}} + local ____opt_2 = a.b + local ____opt_0 = ____opt_2 and ____opt_2.c + return ____opt_0 and ____opt_0.d.e.f +end +return ____exports" +`; + +exports[`optional chaining ("{ foo: \\"foo\\" }") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local obj = {foo = "foo"} + return obj and obj.foo +end +return ____exports" +`; + +exports[`optional chaining ("null") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local obj = nil + return obj and obj.foo +end +return ____exports" +`; + +exports[`optional chaining ("undefined") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local obj = nil + return obj and obj.foo +end +return ____exports" +`; + +exports[`optional element function calls 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local obj = { + value = "foobar", + foo = function(v) return v + 10 end + } + local fooKey = "foo" + local barKey = "bar" + local ____opt_0 = obj[barKey] + local ____temp_4 = ____opt_0 and ____opt_0(5) + if ____temp_4 == nil then + local ____opt_2 = obj[fooKey] + ____temp_4 = ____opt_2 and ____opt_2(15) + end + return ____temp_4 +end +return ____exports" +`; + +exports[`unused call 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local result + local obj = {foo = function(self) + result = "bar" + end} + if obj ~= nil then + obj:foo() + end + return result +end +return ____exports" +`; + +exports[`unused expression 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local obj = {foo = "bar"} + if obj ~= nil then + local ____ = obj.foo + end +end +return ____exports" +`; + +exports[`unused result with preceding statements on right side 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i = 0 + local obj = nil + if obj ~= nil then + local ____opt_0_foo_2 = obj.foo + local ____i_1 = i + i = ____i_1 + 1 + ____opt_0_foo_2(obj, ____i_1) + end + return i +end +return ____exports" +`; + +exports[`unused result with preceding statements on right side 2`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i = 0 + local obj = {foo = function(self, val) + return val + end} + if obj ~= nil then + local ____opt_0_foo_2 = obj.foo + local ____i_1 = i + i = ____i_1 + 1 + ____opt_0_foo_2(obj, ____i_1) + end + return i +end +return ____exports" +`; + +exports[`with preceding statements on right side 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i = 0 + local obj = nil + local ____opt_result_4 + if obj ~= nil then + local ____opt_0_foo_2 = obj.foo + local ____i_1 = i + i = ____i_1 + 1 + ____opt_result_4 = ____opt_0_foo_2(obj, ____i_1) + end + return {result = ____opt_result_4, i = i} +end +return ____exports" +`; + +exports[`with preceding statements on right side 2`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i = 0 + local obj = {foo = function(____, v) return v end} + local ____opt_result_4 + if obj ~= nil then + local ____opt_0_foo_2 = obj.foo + local ____i_1 = i + i = ____i_1 + 1 + ____opt_result_4 = ____opt_0_foo_2(obj, ____i_1) + end + return {result = ____opt_result_4, i = i} +end +return ____exports" +`; + +exports[`with preceding statements on right side modifying left 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i = 0 + local obj = nil + local function bar(self) + if obj then + obj.foo = nil + end + obj = nil + return 1 + end + local ____opt_0 = obj + if ____opt_0 ~= nil then + local ____opt_0_foo_3 = ____opt_0.foo + local ____bar_result_2 = bar(nil) + local ____i_1 = i + i = ____i_1 + 1 + ____opt_0 = ____opt_0_foo_3(____opt_0, ____bar_result_2, ____i_1) + end + return {result = ____opt_0, obj = obj, i = i} +end +return ____exports" +`; + +exports[`with preceding statements on right side modifying left 2`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i = 0 + local obj = {foo = function(self, v) + return v + end} + local function bar(self) + if obj then + obj.foo = nil + end + obj = nil + return 1 + end + local ____opt_0 = obj + if ____opt_0 ~= nil then + local ____opt_0_foo_3 = ____opt_0.foo + local ____bar_result_2 = bar(nil) + local ____i_1 = i + i = ____i_1 + 1 + ____opt_0 = ____opt_0_foo_3(____opt_0, ____bar_result_2, ____i_1) + end + return {result = ____opt_0, obj = obj, i = i} +end +return ____exports" +`; diff --git a/test/unit/__snapshots__/spread.spec.ts.snap b/test/unit/__snapshots__/spread.spec.ts.snap new file mode 100644 index 000000000..c1fba08eb --- /dev/null +++ b/test/unit/__snapshots__/spread.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vararg spread optimization $multi 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function multi(self, ...) + return ... + end + local function test(self, ...) + return select( + 2, + multi(nil, ...) + ) + end + return test(nil, "a", "b", "c") +end +return ____exports" +`; diff --git a/test/unit/__snapshots__/statements.spec.ts.snap b/test/unit/__snapshots__/statements.spec.ts.snap new file mode 100644 index 000000000..d16dde3a7 --- /dev/null +++ b/test/unit/__snapshots__/statements.spec.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Unsupported node adds diagnostic: code 1`] = ` +"a = function() +end" +`; + +exports[`Unsupported node adds diagnostic: diagnostics 1`] = `"main.ts(3,13): error TSTL: Unsupported node kind LabeledStatement"`; diff --git a/test/unit/__snapshots__/switch.spec.ts.snap b/test/unit/__snapshots__/switch.spec.ts.snap new file mode 100644 index 000000000..a64e3f77d --- /dev/null +++ b/test/unit/__snapshots__/switch.spec.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`switch empty fallthrough to default (0) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local out = {} + repeat + local ____switch3 = 0 + local ____cond3 = ____switch3 == 1 + do + out[#out + 1] = "default" + end + until true + return out +end +return ____exports" +`; + +exports[`switch empty fallthrough to default (1) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local out = {} + repeat + local ____switch3 = 1 + local ____cond3 = ____switch3 == 1 + do + out[#out + 1] = "default" + end + until true + return out +end +return ____exports" +`; + +exports[`switch hoisting hoisting from default clause is not duplicated when falling through 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = 1 + local result = "" + repeat + local ____switch3 = x + local hoisted + function hoisted(self) + return "hoisted" + end + local ____cond3 = ____switch3 == 1 + if ____cond3 then + result = hoisted(nil) + break + end + ____cond3 = ____cond3 or ____switch3 == 2 + if ____cond3 then + result = "2" + end + if ____cond3 then + result = "default" + end + ____cond3 = ____cond3 or ____switch3 == 3 + if ____cond3 then + result = "3" + break + end + do + result = "default" + result = "3" + end + until true + return result +end +return ____exports" +`; + +exports[`switch hoisting hoisting from fallthrough clause after default is not duplicated 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = 1 + local result = "" + repeat + local ____switch3 = x + local hoisted + function hoisted(self) + return "hoisted" + end + local ____cond3 = ____switch3 == 1 + if ____cond3 then + result = hoisted(nil) + break + end + ____cond3 = ____cond3 or ____switch3 == 2 + if ____cond3 then + result = "2" + end + if ____cond3 then + result = "default" + end + ____cond3 = ____cond3 or ____switch3 == 3 + if ____cond3 then + result = "3" + break + end + do + result = "default" + result = "3" + end + until true + return result +end +return ____exports" +`; + +exports[`switch produces optimal output 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = 0 + local out = {} + repeat + local ____switch3 = 0 + local ____cond3 = ____switch3 == 0 or ____switch3 == 1 or ____switch3 == 2 + if ____cond3 then + out[#out + 1] = "0,1,2" + break + end + ____cond3 = ____cond3 or ____switch3 == 3 + if ____cond3 then + do + out[#out + 1] = "3" + break + end + end + ____cond3 = ____cond3 or ____switch3 == 4 + if ____cond3 then + break + end + do + x = x + 1 + out[#out + 1] = "default = " .. tostring(x) + do + out[#out + 1] = "3" + break + end + end + until true + out[#out + 1] = tostring(x) + return out +end +return ____exports" +`; diff --git a/test/unit/accessors.spec.ts b/test/unit/accessors.spec.ts deleted file mode 100644 index 608d28851..000000000 --- a/test/unit/accessors.spec.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { Expect, Test } from "alsatian"; -import * as util from "../src/util"; - -export class AccessorTests { - @Test("get accessor") - public getAccessor(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - } - const f = new Foo(); - return f.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - - @Test("get accessor in base class") - public getAccessorInBaseClass(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - } - class Bar extends Foo {} - const b = new Bar(); - return b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - - @Test("get accessor override") - public getAccessorOverride(): void { - const code = - `class Foo { - _foo = "foo"; - foo = "foo"; - } - class Bar extends Foo { - get foo() { return this._foo + "bar"; } - } - const b = new Bar(); - return b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("get accessor overridden") - public getAccessorOverridden(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - } - class Bar extends Foo { - foo = "bar"; - } - const b = new Bar(); - return b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("get accessor override accessor") - public getAccessorOverrideAccessor(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - } - class Bar extends Foo { - _bar = "bar"; - get foo() { return this._bar; } - } - const b = new Bar(); - return b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("get accessor from interface") - public getAccessorFromINterface(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - } - interface Bar { - readonly foo: string; - } - const b: Bar = new Foo(); - return b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - - @Test("set accessor") - public setAccessor(): void { - const code = - `class Foo { - _foo = "foo"; - set foo(val: string) { this._foo = val; } - } - const f = new Foo(); - f.foo = "bar" - return f._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("set accessor in base class") - public setAccessorInBaseClass(): void { - const code = - `class Foo { - _foo = "foo"; - set foo(val: string) { this._foo = val; } - } - class Bar extends Foo {} - const b = new Bar(); - b.foo = "bar" - return b._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("set accessor override") - public setAccessorOverride(): void { - const code = - `class Foo { - _foo = "foo"; - foo = "foo"; - } - class Bar extends Foo { - set foo(val: string) { this._foo = val; } - } - const b = new Bar(); - b.foo = "bar" - return b._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("set accessor overridden") - public setAccessorOverridden(): void { - const code = - `class Foo { - _foo = "baz"; - set foo(val: string) { this._foo = val; } - } - class Bar extends Foo { - foo = "foo"; // triggers base class setter - } - const b = new Bar(); - const fooOriginal = b._foo; - b.foo = "bar" - return fooOriginal + b._foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("set accessor override accessor") - public setAccessorOverrideAccessor(): void { - const code = - `class Foo { - _foo = "foo"; - set foo(val: string) { this._foo = "foo"; } - } - class Bar extends Foo { - set foo(val: string) { this._foo = val; } - } - const b = new Bar(); - b.foo = "bar" - return b._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("set accessor from interface") - public setAccessorFromInterface(): void { - const code = - `class Foo { - _foo = "foo"; - set foo(val: string) { this._foo = val; } - } - interface Bar { - _foo: string; - foo: string; - } - const b: Bar = new Foo(); - b.foo = "bar" - return b._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("get/set accessors") - public getSetAccessor(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - set foo(val: string) { this._foo = val; } - } - const f = new Foo(); - const fooOriginal = f.foo; - f.foo = "bar"; - return fooOriginal + f.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("get/set accessors in base class") - public getSetAccessorInBaseClass(): void { - const code = - `class Foo { - _foo = "foo"; - get foo() { return this._foo; } - set foo(val: string) { this._foo = val; } - } - class Bar extends Foo {} - const b = new Bar(); - const fooOriginal = b.foo; - b.foo = "bar" - return fooOriginal + b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("static get accessor") - public staticGetAccessor(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - } - return Foo.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - - @Test("static get accessor in base class") - public staticGetAccessorInBaseClass(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - } - class Bar extends Foo {} - return Bar.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - - @Test("static get accessor override") - public staticGetAccessorOverride(): void { - const code = - `class Foo { - static _foo = "foo"; - static foo = "foo"; - } - class Bar extends Foo { - static get foo() { return this._foo + "bar"; } - } - return Bar.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("static get accessor overridden") - public staticGetAccessorOverridden(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - } - class Bar extends Foo { - static foo = "bar"; - } - return Bar.foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static get accessor override accessor") - public staticGetAccessorOverrideAccessor(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - } - class Bar extends Foo { - static _bar = "bar"; - static get foo() { return this._bar; } - } - return Bar.foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static get accessor from interface") - public staticGetAccessorFromINterface(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - } - interface Bar { - readonly foo: string; - } - const b: Bar = Foo; - return b.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - - @Test("static set accessor") - public staticSetAccessor(): void { - const code = - `class Foo { - static _foo = "foo"; - static set foo(val: string) { this._foo = val; } - } - Foo.foo = "bar" - return Foo._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static set accessor in base class") - public staticSetAccessorInBaseClass(): void { - const code = - `class Foo { - static _foo = "foo"; - static set foo(val: string) { this._foo = val; } - } - class Bar extends Foo {} - Bar.foo = "bar" - return Bar._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static set accessor override") - public staticSetAccessorOverride(): void { - const code = - `class Foo { - static _foo = "foo"; - static foo = "foo"; - } - class Bar extends Foo { - static set foo(val: string) { this._foo = val; } - } - Bar.foo = "bar" - return Bar._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static set accessor overridden") - public staticSetAccessorOverridden(): void { - const code = - `class Foo { - static _foo = "baz"; - static set foo(val: string) { this._foo = val; } - } - class Bar extends Foo { - static foo = "foo"; // triggers base class setter - } - const fooOriginal = Bar._foo; - Bar.foo = "bar" - return fooOriginal + Bar._foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("static set accessor override accessor") - public staticSetAccessorOverrideAccessor(): void { - const code = - `class Foo { - static _foo = "foo"; - static set foo(val: string) { this._foo = "foo"; } - } - class Bar extends Foo { - static set foo(val: string) { this._foo = val; } - } - Bar.foo = "bar" - return Bar._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static set accessor from interface") - public staticSetAccessorFromInterface(): void { - const code = - `class Foo { - static _foo = "foo"; - static set foo(val: string) { this._foo = val; } - } - interface Bar { - _foo: string; - foo: string; - } - const b: Bar = Foo; - b.foo = "bar" - return b._foo;`; - Expect(util.transpileAndExecute(code)).toBe("bar"); - } - - @Test("static get/set accessors") - public staticGetSetAccessor(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - static set foo(val: string) { this._foo = val; } - } - const fooOriginal = Foo.foo; - Foo.foo = "bar"; - return fooOriginal + Foo.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } - - @Test("static get/set accessors in base class") - public staticGetSetAccessorInBaseClass(): void { - const code = - `class Foo { - static _foo = "foo"; - static get foo() { return this._foo; } - static set foo(val: string) { this._foo = val; } - } - class Bar extends Foo {} - const fooOriginal = Bar.foo; - Bar.foo = "bar" - return fooOriginal + Bar.foo;`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } -} diff --git a/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap b/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap new file mode 100644 index 000000000..11cab115c --- /dev/null +++ b/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IncorrectUsage: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local __TS__New = ____lualib.__TS__New +local ____exports = {} +function ____exports.__main(self) + --- + -- @customConstructor + local Point2D = __TS__Class() + Point2D.name = "Point2D" + function Point2D.prototype.____constructor(self) + end + __TS__New(Point2D) +end +return ____exports" +`; + +exports[`IncorrectUsage: diagnostics 1`] = `"main.ts(5,9): error TSTL: '@customConstructor' expects 1 arguments, but got 0."`; diff --git a/test/unit/annotations/compileMembersOnly.spec.ts b/test/unit/annotations/compileMembersOnly.spec.ts new file mode 100644 index 000000000..5db0fe974 --- /dev/null +++ b/test/unit/annotations/compileMembersOnly.spec.ts @@ -0,0 +1,34 @@ +import * as util from "../../util"; + +test("@compileMembersOnly", () => { + util.testFunction` + /** @compileMembersOnly */ + enum TestEnum { + A = 0, + B = 2, + C, + D = "D", + } + + return { A: TestEnum.A, B: TestEnum.B, C: TestEnum.C, D: TestEnum.D }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("TestEnum")) + .expectToMatchJsResult(); +}); + +test("@compileMembersOnly in a namespace", () => { + util.testModule` + namespace Test { + /** @compileMembersOnly */ + export enum TestEnum { + A = "A", + B = "B", + } + } + + export const A = Test.TestEnum.A; + ` + .setReturnExport("A") + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("Test.A")) + .expectToEqual("A"); +}); diff --git a/test/unit/annotations/customConstructor.spec.ts b/test/unit/annotations/customConstructor.spec.ts new file mode 100644 index 000000000..86858f700 --- /dev/null +++ b/test/unit/annotations/customConstructor.spec.ts @@ -0,0 +1,37 @@ +import { annotationInvalidArgumentCount } from "../../../src/transformation/utils/diagnostics"; +import * as util from "../../util"; + +test("CustomCreate", () => { + const luaHeader = ` + function Point2DCreate(x, y) + return {x = x, y = y} + end + `; + + const tsHeader = ` + /** @customConstructor Point2DCreate */ + class Point2D { + public x: number; + public y: number; + constructor(x: number, y: number) { + // No values assigned + } + } + `; + + // Can't use expectToMatchJsResult because above is not valid TS/JS + util.testModule`export default new Point2D(1, 2).x;` + .setTsHeader(tsHeader) + .setLuaHeader(luaHeader) + .setReturnExport("default") + .expectToEqual(1); +}); + +test("IncorrectUsage", () => { + util.testFunction` + /** @customConstructor */ + class Point2D {} + + new Point2D(); + `.expectDiagnosticsToMatchSnapshot([annotationInvalidArgumentCount.code]); +}); diff --git a/test/unit/array.spec.ts b/test/unit/array.spec.ts deleted file mode 100644 index eecdf4cd1..000000000 --- a/test/unit/array.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; - -export class ArrayTests { - @Test("Array access") - public arrayAccess(): void { - const result = util.transpileAndExecute( - `const arr: number[] = [3,5,1]; - return arr[1];` - ); - Expect(result).toBe(5); - } - - @Test("Array union access") - public arrayUnionAccess(): void { - const result = util.transpileAndExecute( - `function makeArray(): number[] | string[] { return [3,5,1]; } - const arr = makeArray(); - return arr[1];` - ); - Expect(result).toBe(5); - } - - @Test("Array union length") - public arrayUnionLength(): void { - const result = util.transpileAndExecute( - `function makeArray(): number[] | string[] { return [3,5,1]; } - const arr = makeArray(); - return arr.length;` - ); - Expect(result).toBe(3); - } - - @Test("Array intersection access") - public arrayIntersectionAccess(): void { - const result = util.transpileAndExecute( - `type I = number[] & {foo: string}; - function makeArray(): I { - let t = [3,5,1]; - (t as I).foo = "bar"; - return (t as I); - } - const arr = makeArray(); - return arr[1];` - ); - Expect(result).toBe(5); - } - - @Test("Array intersection length") - public arrayIntersectionLength(): void { - const result = util.transpileAndExecute( - `type I = number[] & {foo: string}; - function makeArray(): I { - let t = [3,5,1]; - (t as I).foo = "bar"; - return (t as I); - } - const arr = makeArray(); - return arr.length;` - ); - Expect(result).toBe(3); - } - - @TestCase("firstElement()", 3) - @TestCase("name", "array") - @TestCase("length", 1) - @Test("Derived array access") - public derivedArrayAccess(member: string, expected: any): void { - const luaHeader = `local arr = {name="array", firstElement=function(self) return self[1]; end};`; - const typeScriptHeader = - `interface CustomArray extends Array{ - name:string, - firstElement():number; - }; - declare const arr: CustomArray;`; - - const result = util.transpileAndExecute( - ` - arr[0] = 3; - return arr.${member};`, - undefined, - luaHeader, - typeScriptHeader - ); - - Expect(result).toBe(expected); - } - - @Test("Array delete") - public arrayDelete(): void { - const result = util.transpileAndExecute( - `const myarray = [1,2,3,4]; - delete myarray[2]; - return \`\${myarray[0]},\${myarray[1]},\${myarray[2]},\${myarray[3]}\`;` - ); - - Expect(result).toBe("1,2,nil,4"); - } - - @Test("Array delete return true") - public arrayDeleteReturnTrue(): void { - const result = util.transpileAndExecute( - `const myarray = [1,2,3,4]; - const exists = delete myarray[2]; - return \`\${exists}:\${myarray[0]},\${myarray[1]},\${myarray[2]},\${myarray[3]}\`;` - ); - - Expect(result).toBe("true:1,2,nil,4"); - } - - @Test("Array delete return false") - public arrayDeleteReturnFalse(): void { - const result = util.transpileAndExecute( - `const myarray = [1,2,3,4]; - const exists = delete myarray[4]; - return \`\${exists}:\${myarray[0]},\${myarray[1]},\${myarray[2]},\${myarray[3]}\`;` - ); - - Expect(result).toBe("true:1,2,3,4"); - } - - @Test("Array property access") - public arrayPropertyAccess(): void { - const code = - `type A = number[] & {foo?: string}; - const a: A = [1,2,3]; - a.foo = "bar"; - return \`\${a.foo}\${a[0]}\${a[1]}\${a[2]}\`;`; - Expect(util.transpileAndExecute(code)).toBe("bar123"); - } -} diff --git a/test/unit/assignmentDestructuring.spec.ts b/test/unit/assignmentDestructuring.spec.ts deleted file mode 100644 index 7f8e10507..000000000 --- a/test/unit/assignmentDestructuring.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { LuaTarget, LuaLibImportKind } from "../../src/CompilerOptions"; -import * as util from "../src/util"; - -export class AssignmentDestructuringTests { - - private readonly assignmentDestruturingTs = ` - declare function myFunc(): [number, string]; - let [a, b] = myFunc();`; - - @Test("Assignment destructuring [5.1]") - public assignmentDestructuring51(): void { - // Transpile - const lua = util.transpileString( - this.assignmentDestruturingTs, {luaTarget: LuaTarget.Lua51, luaLibImport: LuaLibImportKind.None} - ); - // Assert - Expect(lua).toBe(`local a, b = unpack(myFunc());`); - } - - @Test("Assignment destructuring [5.2]") - public tupleDestructing52(): void { - // Transpile - const lua = util.transpileString( - this.assignmentDestruturingTs, {luaTarget: LuaTarget.Lua52, luaLibImport: LuaLibImportKind.None} - ); - // Assert - Expect(lua).toBe(`local a, b = table.unpack(myFunc());`); - } - - @Test("Assignment destructuring [JIT]") - public assignmentDestructuringJIT(): void { - // Transpile - const lua = util.transpileString( - this.assignmentDestruturingTs, {luaTarget: LuaTarget.LuaJIT, luaLibImport: LuaLibImportKind.None} - ); - // Assert - Expect(lua).toBe(`local a, b = unpack(myFunc());`); - } -} diff --git a/test/unit/assignments.spec.ts b/test/unit/assignments.spec.ts index dda85cc20..53291f50f 100644 --- a/test/unit/assignments.spec.ts +++ b/test/unit/assignments.spec.ts @@ -1,977 +1,518 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { TranspileError } from "../../src/TranspileError"; - -import * as util from "../src/util"; -const fs = require("fs"); - -export class AssignmentTests { - - public static readonly funcAssignTestCode = - `let func: {(s: string): string} = function(s) { return s + "+func"; }; - let lambda: (s: string) => string = s => s + "+lambda"; - let thisFunc: {(this: Foo, s: string): string} = function(s) { return s + "+thisFunc"; }; - let thisLambda: (this: Foo, s: string) => string = s => s + "+thisLambda"; - class Foo { - method(s: string): string { return s + "+method"; } - lambdaProp: (s: string) => string = s => s + "+lambdaProp"; - voidMethod(this: void, s: string): string { return s + "+voidMethod"; } - voidLambdaProp: (this: void, s: string) => string = s => s + "+voidLambdaProp"; - static voidStaticMethod(this: void, s: string): string { return s + "+voidStaticMethod"; } - static voidStaticLambdaProp: (this: void, s: string) => string = s => s + "+voidStaticLambdaProp"; - static staticMethod(s: string): string { return s + "+staticMethod"; } - static staticLambdaProp: (s: string) => string = s => s + "+staticLambdaProp"; - } - const foo = new Foo();`; - - @TestCase(`"abc"`, `"abc"`) - @TestCase("3", "3") - @TestCase("[1,2,3]", "{1, 2, 3}") - @TestCase("true", "true") - @TestCase("false", "false") - @TestCase(`{a:3,b:"4"}`, `{a = 3, b = "4"}`) - @Test("Const assignment") - public constAssignment(inp: string, out: string): void { - const lua = util.transpileString(`const myvar = ${inp};`); - Expect(lua).toBe(`local myvar = ${out};`); - } - - @TestCase(`"abc"`, `"abc"`) - @TestCase("3", "3") - @TestCase("[1,2,3]", "{1, 2, 3}") - @TestCase("true", "true") - @TestCase("false", "false") - @TestCase(`{a:3,b:"4"}`, `{a = 3, b = "4"}`) - @Test("Let assignment") - public letAssignment(inp: string, out: string): void { - const lua = util.transpileString(`let myvar = ${inp};`); - Expect(lua).toBe(`local myvar = ${out};`); - } - - @TestCase(`"abc"`, `"abc"`) - @TestCase("3", "3") - @TestCase("[1,2,3]", "{1, 2, 3}") - @TestCase("true", "true") - @TestCase("false", "false") - @TestCase(`{a:3,b:"4"}`, `{a = 3, b = "4"}`) - @Test("Var assignment") - public varAssignment(inp: string, out: string): void { - const lua = util.transpileString(`var myvar = ${inp};`); - Expect(lua).toBe(`myvar = ${out};`); - } - - @TestCase("var myvar;") - @TestCase("let myvar;") - @TestCase("const myvar = null;") - @TestCase("const myvar = undefined;") - @Test("Null assignments") - public nullAssignment(declaration: string): void { - const result = util.transpileAndExecute(declaration + " return myvar;"); - Expect(result).toBe(undefined); - } - - @TestCase(["a", "b"], ["e", "f"]) - @TestCase(["a", "b"], ["e", "f", "g"]) - @TestCase(["a", "b", "c"], ["e", "f", "g"]) - @Test("Binding pattern assignment") - public bindingPattern(input: string[], values: string[]): void { - const pattern = input.join(","); - const initializer = values.map(v => `"${v}"`).join(","); - - const tsCode = `const [${pattern}] = [${initializer}]; return [${pattern}].join("-");`; - const result = util.transpileAndExecute(tsCode); - - Expect(result).toBe(values.slice(0, input.length).join("-")); - } - - @Test("Ellipsis binding pattern") - public ellipsisBindingPattern(): void { - Expect(() => util.transpileString("let [a,b,...c] = [1,2,3];")) - .toThrowError(Error, "Ellipsis destruction is not allowed."); - } - - @Test("Tuple Assignment") - public tupleAssignment(): void { - const code = `function abc(): [number, number] { return [1, 2]; }; - let t: [number, number] = abc(); - return t[0] + t[1];`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(3); - } - - @Test("TupleReturn assignment") - public tupleReturnFunction(): void { - const code = `/** @tupleReturn */\n` - + `declare function abc(): number[]\n` - + `let [a,b] = abc();`; - - const lua = util.transpileString(code); - Expect(lua).toBe("local a, b = abc();"); - } - - @Test("TupleReturn Single assignment") - public tupleReturnSingleAssignment(): void { - const code = `/** @tupleReturn */\n` - + `declare function abc(): [number, string];\n` - + `let a = abc();` - + `a = abc();`; - - const lua = util.transpileString(code); - Expect(lua).toBe("local a = ({abc()});\na = ({abc()});"); - } - - @Test("TupleReturn interface assignment") - public tupleReturnInterface(): void { - const code = `interface def {\n` - + `/** @tupleReturn */\n` - + `abc();\n` - + `} declare const jkl : def;\n` - + `let [a,b] = jkl.abc();`; - - const lua = util.transpileString(code); - Expect(lua).toBe("local a, b = jkl:abc();"); - } - - @Test("TupleReturn namespace assignment") - public tupleReturnNameSpace(): void { - const code = `declare namespace def {\n` - + `/** @tupleReturn */\n` - + `function abc() {}\n` - + `}\n` - + `let [a,b] = def.abc();`; - - const lua = util.transpileString(code); - Expect(lua).toBe("local a, b = def.abc();"); - } - - @Test("TupleReturn method assignment") - public tupleReturnMethod(): void { - const code = `declare class def {\n` - + `/** @tupleReturn */\n` - + `abc() { return [1,2,3]; }\n` - + `} const jkl = new def();\n` - + `let [a,b] = jkl.abc();`; - - const lua = util.transpileString(code); - Expect(lua).toBe("local jkl = def.new();\nlocal a, b = jkl:abc();"); - } - - @Test("TupleReturn functional") - public tupleReturnFunctional(): void { - const code = `/** @tupleReturn */ - function abc(): [number, string] { return [3, "a"]; } - const [a, b] = abc(); - return b + a;`; - - const result = util.transpileAndExecute(code); - - Expect(result).toBe("a3"); - } - - @Test("TupleReturn single") - public tupleReturnSingle(): void { - const code = `/** @tupleReturn */ - function abc(): [number, string] { return [3, "a"]; } - const res = abc(); - return res.length`; - - const result = util.transpileAndExecute(code); - - Expect(result).toBe(2); - } - - @Test("TupleReturn in expression") - public tupleReturnInExpression(): void { - const code = `/** @tupleReturn */ - function abc(): [number, string] { return [3, "a"]; } - return abc()[1] + abc()[0];`; - - const result = util.transpileAndExecute(code); - - Expect(result).toBe("a3"); - } - - @TestCase("and") - @TestCase("local") - @TestCase("nil") - @TestCase("not") - @TestCase("or") - @TestCase("repeat") - @TestCase("then") - @TestCase("until") - @Test("Keyword identifier error") - public keywordIdentifierError(identifier: string): void { - Expect(() => util.transpileString(`const ${identifier} = 3;`)) - .toThrowError(TranspileError, `Cannot use Lua keyword ${identifier} as identifier.`); - } - - @TestCase("func", "lambda", "foo+lambda") - @TestCase("func", "s => s", "foo") - @TestCase("func", "(s => s)", "foo") - @TestCase("func", "function(s) { return s; }", "foo") - @TestCase("func", "(function(s) { return s; })", "foo") - @TestCase("func", "function(this: void, s: string) { return s; }", "foo") - @TestCase("func", "s => foo.method(s)", "foo+method") - @TestCase("func", "s => foo.lambdaProp(s)", "foo+lambdaProp") - @TestCase("func", "Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("func", "Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("func", "foo.voidMethod", "foo+voidMethod") - @TestCase("func", "foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("lambda", "func", "foo+func") - @TestCase("lambda", "s => s", "foo") - @TestCase("lambda", "(s => s)", "foo") - @TestCase("lambda", "function(s) { return s; }", "foo") - @TestCase("lambda", "(function(s) { return s; })", "foo") - @TestCase("lambda", "function(this: void, s: string) { return s; }", "foo") - @TestCase("lambda", "s => foo.method(s)", "foo+method") - @TestCase("lambda", "s => foo.lambdaProp(s)", "foo+lambdaProp") - @TestCase("lambda", "Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("lambda", "Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("lambda", "foo.voidMethod", "foo+voidMethod") - @TestCase("lambda", "foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("Foo.voidStaticMethod", "func", "foo+func") - @TestCase("Foo.voidStaticMethod", "lambda", "foo+lambda") - @TestCase("Foo.voidStaticMethod", "s => s", "foo") - @TestCase("Foo.voidStaticMethod", "(s => s)", "foo") - @TestCase("Foo.voidStaticMethod", "function(s) { return s; }", "foo") - @TestCase("Foo.voidStaticMethod", "(function(s) { return s; })", "foo") - @TestCase("Foo.voidStaticMethod", "function(this: void, s: string) { return s; }", "foo") - @TestCase("Foo.voidStaticMethod", "s => foo.method(s)", "foo+method") - @TestCase("Foo.voidStaticMethod", "s => foo.lambdaProp(s)", "foo+lambdaProp") - @TestCase("Foo.voidStaticMethod", "Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("Foo.voidStaticMethod", "foo.voidMethod", "foo+voidMethod") - @TestCase("Foo.voidStaticMethod", "foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("Foo.voidStaticLambdaProp", "func", "foo+func") - @TestCase("Foo.voidStaticLambdaProp", "lambda", "foo+lambda") - @TestCase("Foo.voidStaticLambdaProp", "s => s", "foo") - @TestCase("Foo.voidStaticLambdaProp", "(s => s)", "foo") - @TestCase("Foo.voidStaticLambdaProp", "function(s) { return s; }", "foo") - @TestCase("Foo.voidStaticLambdaProp", "(function(s) { return s; })", "foo") - @TestCase("Foo.voidStaticLambdaProp", "function(this: void, s: string) { return s; }", "foo") - @TestCase("Foo.voidStaticLambdaProp", "s => foo.method(s)", "foo+method") - @TestCase("Foo.voidStaticLambdaProp", "s => foo.lambdaProp(s)", "foo+lambdaProp") - @TestCase("Foo.voidStaticLambdaProp", "Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("Foo.voidStaticLambdaProp", "foo.voidMethod", "foo+voidMethod") - @TestCase("Foo.voidStaticLambdaProp", "foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("foo.voidMethod", "func", "foo+func") - @TestCase("foo.voidMethod", "lambda", "foo+lambda") - @TestCase("foo.voidMethod", "s => s", "foo") - @TestCase("foo.voidMethod", "(s => s)", "foo") - @TestCase("foo.voidMethod", "function(s) { return s; }", "foo") - @TestCase("foo.voidMethod", "(function(s) { return s; })", "foo") - @TestCase("foo.voidMethod", "function(this: void, s: string) { return s; }", "foo") - @TestCase("foo.voidMethod", "s => foo.method(s)", "foo+method") - @TestCase("foo.voidMethod", "s => foo.lambdaProp(s)", "foo+lambdaProp") - @TestCase("foo.voidMethod", "Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("foo.voidMethod", "Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("foo.voidMethod", "foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("foo.voidLambdaProp", "func", "foo+func") - @TestCase("foo.voidLambdaProp", "lambda", "foo+lambda") - @TestCase("foo.voidLambdaProp", "s => s", "foo") - @TestCase("foo.voidLambdaProp", "(s => s)", "foo") - @TestCase("foo.voidLambdaProp", "function(s) { return s; }", "foo") - @TestCase("foo.voidLambdaProp", "(function(s) { return s; })", "foo") - @TestCase("foo.voidLambdaProp", "function(this: void, s: string) { return s; }", "foo") - @TestCase("foo.voidLambdaProp", "s => foo.method(s)", "foo+method") - @TestCase("foo.voidLambdaProp", "s => foo.lambdaProp(s)", "foo+lambdaProp") - @TestCase("foo.voidLambdaProp", "Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("foo.voidLambdaProp", "Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("foo.voidLambdaProp", "foo.voidMethod", "foo+voidMethod") - @TestCase("func", "<(s: string) => string>lambda", "foo+lambda") - @TestCase("func", "lambda as ((s: string) => string)", "foo+lambda") - @Test("Valid function assignment") - public validFunctionAssignment(func: string, assignTo: string, expectResult: string): void { - const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo}; return ${func}("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("func", "foo+func") - @TestCase("lambda", "foo+lambda") - @TestCase("Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("foo.voidMethod", "foo+voidMethod") - @TestCase("foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("s => s", "foo") - @TestCase("(s => s)", "foo") - @TestCase("function(s) { return s; }", "foo") - @TestCase("(function(s) { return s; })", "foo") - @TestCase("function(this: void, s: string) { return s; }", "foo") - @TestCase("func", "foo+func", "string | ((s: string) => string)") - @TestCase("func", "foo+func", "T") - @TestCase("<(s: string) => string>func", "foo+func") - @TestCase("func as ((s: string) => string)", "foo+func") - @Test("Valid function argument") - public validFunctionArgument(func: string, expectResult: string, funcType?: string): void { - if (!funcType) { - funcType = "(s: string) => string"; - } - const code = `${AssignmentTests.funcAssignTestCode} - function takesFunc string)>(fn: ${funcType}) { - return (fn as any)("foo"); - } - return takesFunc(${func});`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("s => s", "foo") - @TestCase("(s => s)", "foo") - @TestCase("function(s) { return s; }", "foo") - @TestCase("(function(s) { return s; })", "foo") - @TestCase("function(this: void, s: string) { return s; }", "foo") - @Test("Valid function expression argument with no signature") - public validFunctionExpressionArgumentNoSignature(func: string, expectResult: string): void { - const code = `${AssignmentTests.funcAssignTestCode} - const takesFunc: any = (fn: (s: string) => string) => { - return (fn as any)("foo"); - } - return takesFunc(${func});`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("func", "foo+func") - @TestCase("lambda", "foo+lambda") - @TestCase("Foo.voidStaticMethod", "foo+voidStaticMethod") - @TestCase("Foo.voidStaticLambdaProp", "foo+voidStaticLambdaProp") - @TestCase("foo.voidMethod", "foo+voidMethod") - @TestCase("foo.voidLambdaProp", "foo+voidLambdaProp") - @TestCase("s => s", "foo") - @TestCase("(s => s)", "foo") - @TestCase("function(s) { return s; }", "foo") - @TestCase("(function(s) { return s; })", "foo") - @TestCase("function(this: void, s: string) { return s; }", "foo") - @TestCase("func", "foo+func", "string | ((s: string) => string)") - @TestCase("<(s: string) => string>func", "foo+func") - @TestCase("func as ((s: string) => string)", "foo+func") - @Test("Valid function return") - public validFunctionReturn(func: string, expectResult: string, funcType?: string): void { - if (!funcType) { - funcType = "(s: string) => string"; - } - const code = `${AssignmentTests.funcAssignTestCode} - function returnsFunc(): ${funcType} { - return ${func}; - } - const fn = returnsFunc(); - return (fn as any)("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("foo.method", "foo.lambdaProp", "foo+lambdaProp") - @TestCase("foo.method", "s => s", "foo") - @TestCase("foo.method", "function(s) { return s; }", "foo") - @TestCase("foo.method", "(function(s) { return s; })", "foo") - @TestCase("foo.method", "function(this: Foo, s: string) { return s; }", "foo") - @TestCase("foo.method", "s => func(s)", "foo+func") - @TestCase("foo.method", "s => lambda(s)", "foo+lambda") - @TestCase("foo.method", "Foo.staticMethod", "foo+staticMethod") - @TestCase("foo.method", "Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("foo.method", "thisFunc", "foo+thisFunc") - @TestCase("foo.method", "thisLambda", "foo+thisLambda") - @TestCase("foo.lambdaProp", "foo.method", "foo+method") - @TestCase("foo.lambdaProp", "s => s", "foo") - @TestCase("foo.lambdaProp", "(s => s)", "foo") - @TestCase("foo.lambdaProp", "function(s) { return s; }", "foo") - @TestCase("foo.lambdaProp", "(function(s) { return s; })", "foo") - @TestCase("foo.lambdaProp", "function(this: Foo, s: string) { return s; }", "foo") - @TestCase("foo.lambdaProp", "s => func(s)", "foo+func") - @TestCase("foo.lambdaProp", "s => lambda(s)", "foo+lambda") - @TestCase("foo.lambdaProp", "Foo.staticMethod", "foo+staticMethod") - @TestCase("foo.lambdaProp", "Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("foo.lambdaProp", "thisFunc", "foo+thisFunc") - @TestCase("foo.lambdaProp", "thisLambda", "foo+thisLambda") - @TestCase("Foo.staticMethod", "foo.method", "foo+method") - @TestCase("Foo.staticMethod", "foo.lambdaProp", "foo+lambdaProp") - @TestCase("Foo.staticMethod", "s => s", "foo") - @TestCase("Foo.staticMethod", "(s => s)", "foo") - @TestCase("Foo.staticMethod", "function(s) { return s; }", "foo") - @TestCase("Foo.staticMethod", "(function(s) { return s; })", "foo") - @TestCase("Foo.staticMethod", "function(this: Foo, s: string) { return s; }", "foo") - @TestCase("Foo.staticMethod", "s => func(s)", "foo+func") - @TestCase("Foo.staticMethod", "s => lambda(s)", "foo+lambda") - @TestCase("Foo.staticMethod", "Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("Foo.staticMethod", "thisFunc", "foo+thisFunc") - @TestCase("Foo.staticMethod", "thisLambda", "foo+thisLambda") - @TestCase("Foo.staticLambdaProp", "foo.method", "foo+method") - @TestCase("Foo.staticLambdaProp", "foo.lambdaProp", "foo+lambdaProp") - @TestCase("Foo.staticLambdaProp", "s => s", "foo") - @TestCase("Foo.staticLambdaProp", "(s => s)", "foo") - @TestCase("Foo.staticLambdaProp", "function(s) { return s; }", "foo") - @TestCase("Foo.staticLambdaProp", "(function(s) { return s; })", "foo") - @TestCase("Foo.staticLambdaProp", "function(this: Foo, s: string) { return s; }", "foo") - @TestCase("Foo.staticLambdaProp", "s => func(s)", "foo+func") - @TestCase("Foo.staticLambdaProp", "s => lambda(s)", "foo+lambda") - @TestCase("Foo.staticLambdaProp", "Foo.staticMethod", "foo+staticMethod") - @TestCase("Foo.staticLambdaProp", "thisFunc", "foo+thisFunc") - @TestCase("Foo.staticLambdaProp", "thisLambda", "foo+thisLambda") - @TestCase("thisFunc", "foo.method", "foo+method") - @TestCase("thisFunc", "foo.lambdaProp", "foo+lambdaProp") - @TestCase("thisFunc", "s => s", "foo") - @TestCase("thisFunc", "(s => s)", "foo") - @TestCase("thisFunc", "function(s) { return s; }", "foo") - @TestCase("thisFunc", "(function(s) { return s; })", "foo") - @TestCase("thisFunc", "function(this: Foo, s: string) { return s; }", "foo") - @TestCase("thisFunc", "s => func(s)", "foo+func") - @TestCase("thisFunc", "s => lambda(s)", "foo+lambda") - @TestCase("thisFunc", "Foo.staticMethod", "foo+staticMethod") - @TestCase("thisFunc", "Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("thisFunc", "thisLambda", "foo+thisLambda") - @TestCase("thisLambda", "foo.method", "foo+method") - @TestCase("thisLambda", "foo.lambdaProp", "foo+lambdaProp") - @TestCase("thisLambda", "s => s", "foo") - @TestCase("thisLambda", "(s => s)", "foo") - @TestCase("thisLambda", "function(s) { return s; }", "foo") - @TestCase("thisLambda", "(function(s) { return s; })", "foo") - @TestCase("thisLambda", "function(this: Foo, s: string) { return s; }", "foo") - @TestCase("thisLambda", "s => func(s)", "foo+func") - @TestCase("thisLambda", "s => lambda(s)", "foo+lambda") - @TestCase("thisLambda", "Foo.staticMethod", "foo+staticMethod") - @TestCase("thisLambda", "Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("thisLambda", "thisFunc", "foo+thisFunc") - @TestCase("foo.method", "<(this: Foo, s: string) => string>foo.lambdaProp", "foo+lambdaProp") - @TestCase("foo.method", "foo.lambdaProp as ((this: Foo, s: string) => string)", "foo+lambdaProp") - @Test("Valid method assignment") - public validMethodAssignment(func: string, assignTo: string, expectResult: string): void { - const code = `${AssignmentTests.funcAssignTestCode} - ${func} = ${assignTo}; - foo.method = ${func}; - return foo.method("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } +import { unsupportedVarDeclaration } from "../../src/transformation/utils/diagnostics"; +import * as util from "../util"; - @TestCase("foo.method", "foo+method") - @TestCase("foo.lambdaProp", "foo+lambdaProp") - @TestCase("Foo.staticMethod", "foo+staticMethod") - @TestCase("Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("thisFunc", "foo+thisFunc") - @TestCase("thisLambda", "foo+thisLambda") - @TestCase("s => s", "foo") - @TestCase("(s => s)", "foo") - @TestCase("function(s) { return s; }", "foo") - @TestCase("(function(s) { return s; })", "foo") - @TestCase("function(this: Foo, s: string) { return s; }", "foo") - @TestCase("foo.method", "foo+method", "string | ((this: Foo, s: string) => string)") - @TestCase("foo.method", "foo+method", "T") - @TestCase("<(this: Foo, s: string) => string>foo.method", "foo+method") - @TestCase("foo.method as ((this: Foo, s: string) => string)", "foo+method") - @Test("Valid method argument") - public validMethodArgument(func: string, expectResult: string, funcType?: string): void { - if (!funcType) { - funcType = "(this: Foo, s: string) => string"; +test.each(["const", "let"])("%s declaration not top-level is not global", declarationKind => { + util.testModule` + { + ${declarationKind} foo = true; } - const code = `${AssignmentTests.funcAssignTestCode} - function takesMethod string)>(meth: ${funcType}) { - foo.method = meth as any; - } - takesMethod(${func}); - return foo.method("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("foo.method", "foo+method") - @TestCase("foo.lambdaProp", "foo+lambdaProp") - @TestCase("Foo.staticMethod", "foo+staticMethod") - @TestCase("Foo.staticLambdaProp", "foo+staticLambdaProp") - @TestCase("thisFunc", "foo+thisFunc") - @TestCase("thisLambda", "foo+thisLambda") - @TestCase("s => s", "foo") - @TestCase("(s => s)", "foo") - @TestCase("function(s) { return s; }", "foo") - @TestCase("(function(s) { return s; })", "foo") - @TestCase("function(this: Foo, s: string) { return s; }", "foo") - @TestCase("foo.method", "foo+method", "string | ((this: Foo, s: string) => string)") - @TestCase("<(this: Foo, s: string) => string>foo.method", "foo+method") - @TestCase("foo.method as ((this: Foo, s: string) => string)", "foo+method") - @Test("Valid method return") - public validMethodReturn(func: string, expectResult: string, funcType?: string): void { - if (!funcType) { - funcType = "(this: Foo, s: string) => string"; + // @ts-ignore + return "foo" in globalThis; + `.expectToEqual(false); +}); + +test.each(["const", "let"])("top-level %s declaration is global", declarationKind => { + // Can't be tested with expectToMatchJsResult because in JS that would not be global + util.testModule` + import './a'; + export const result = foo; + ` + .addExtraFile("a.ts", `${declarationKind} foo = true;`) + .expectToEqual({ result: true }); +}); + +describe("var is disallowed", () => { + test("var declaration", () => { + util.testFunction` + var foo = true; + `.expectDiagnosticsToMatchSnapshot([unsupportedVarDeclaration.code]); + }); + + test("for loop", () => { + util.testFunction` + for (var foo = 0;;) {} + `.expectDiagnosticsToMatchSnapshot([unsupportedVarDeclaration.code]); + }); + + test("for...in loop", () => { + util.testFunction` + for (var foo in {}) {} + `.expectDiagnosticsToMatchSnapshot([unsupportedVarDeclaration.code]); + }); + + test("for...of loop", () => { + util.testFunction` + for (var foo of []) {} + `.expectDiagnosticsToMatchSnapshot([unsupportedVarDeclaration.code]); + }); +}); + +test.each(["let result;", "const result = null;", "const result = undefined;"])( + "Null assignments (%p)", + declaration => { + util.testFunction` + ${declaration} + return result; + `.expectToEqual(undefined); + } +); + +test.each(["x = y", "x += y"])("Assignment expressions (%p)", expression => { + util.testFunction` + let x = "x"; + let y = "y"; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test.each(["x = o.p", "x = a[0]", "x = y = o.p", "x = o.p"])("Assignment expressions using temp (%p)", expression => { + util.testFunction` + let x = "x"; + let y = "y"; + let o = {p: "o"}; + let a = ["a"]; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test.each(["o.p = x", "a[0] = x", "o.p = a[0]", "o.p = a[0] = x"])( + "Property assignment expressions (%p)", + expression => { + util.testFunction` + let x = "x"; + let o = {p: "o"}; + let a = ["a"]; + return ${expression}; + `.expectToMatchJsResult(); + } +); + +test.each([ + "x = t()", + "x = tr()", + "[x[1], x[0]] = t()", + "[x[1], x[0]] = tr()", + "x = [y[1], y[0]]", + "[x[0], x[1]] = [y[1], y[0]]", +])("multi return assignment expressions (%p)", expression => { + util.testFunction` + let x: [string, string] = ["x0", "x1"]; + let y: [string, string] = ["y0", "y1"]; + function t(): [string, string] { return ["t0", "t1"] }; + function tr(): LuaMultiReturn<[string, string]> { return $multi("tr0", "tr1"); }; + const r = ${expression}; + return \`\${r[0]},\${r[1]}\` + ` + .withLanguageExtensions() + .expectToMatchJsResult(); +}); + +test.each([ + "++x", + "x++", + "--x", + "x--", + "x += y", + "x -= y", + "x *= y", + "y /= x", + "y %= x", + "y **= x", + "x |= y", + "x &= y", + "x ^= y", + "x <<= y", + "x >>>= y", + "x &&= y", + "x ||= y", + "x ??= y", +])("Operator assignment statements (%p)", statement => { + util.testFunction` + let x = 3; + let y = 6; + ${statement}; + return { x, y }; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1277 +test("Compound assignment as expression (#1277)", () => { + util.testFunction` + let foo = { + bar: false as any } - const code = `${AssignmentTests.funcAssignTestCode} - function returnMethod(): ${funcType} { - return ${func}; - } - foo.method = returnMethod() as any; - return foo.method("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("func", "foo.method") - @TestCase("func", "foo.lambdaProp") - @TestCase("func", "Foo.staticMethod") - @TestCase("func", "Foo.staticLambdaProp") - @TestCase("func", "function(this: Foo, s: string) { return s; }") - @TestCase("lambda", "foo.method") - @TestCase("lambda", "foo.lambdaProp") - @TestCase("lambda", "Foo.staticMethod") - @TestCase("lambda", "Foo.staticLambdaProp") - @TestCase("lambda", "function(this: Foo, s: string) { return s; }") - @TestCase("foo.voidMethod", "foo.method") - @TestCase("foo.voidMethod", "foo.lambdaProp") - @TestCase("foo.voidMethod", "Foo.staticMethod") - @TestCase("foo.voidMethod", "Foo.staticLambdaProp") - @TestCase("foo.voidMethod", "function(this: Foo, s: string) { return s; }") - @TestCase("foo.voidLambdaProp", "foo.method") - @TestCase("foo.voidLambdaProp", "foo.lambdaProp") - @TestCase("foo.voidLambdaProp", "Foo.staticMethod") - @TestCase("foo.voidLambdaProp", "Foo.staticLambdaProp") - @TestCase("foo.voidLambdaProp", "function(this: Foo, s: string) { return s; }") - @TestCase("Foo.voidStaticMethod", "foo.method") - @TestCase("Foo.voidStaticMethod", "foo.lambdaProp") - @TestCase("Foo.voidStaticMethod", "Foo.staticMethod") - @TestCase("Foo.voidStaticMethod", "Foo.staticLambdaProp") - @TestCase("Foo.voidStaticMethod", "function(this: Foo, s: string) { return s; }") - @TestCase("Foo.voidStaticLambdaProp", "foo.method") - @TestCase("Foo.voidStaticLambdaProp", "foo.lambdaProp") - @TestCase("Foo.voidStaticLambdaProp", "Foo.staticMethod") - @TestCase("Foo.voidStaticLambdaProp", "Foo.staticLambdaProp") - @TestCase("Foo.voidStaticLambdaProp", "function(this: Foo, s: string) { return s; }") - @TestCase("func", "(foo.method as (string | ((this: Foo, s: string) => string)))") - @TestCase("func", "<(s: string) => string>foo.method") - @TestCase("func", "foo.method as ((s: string) => string)") - @Test("Invalid function assignment") - public invalidFunctionAssignment(func: string, assignTo: string): void { - const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo};`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from method to function. To fix, wrap the method in an arrow function."); - } - - @TestCase("foo.method") - @TestCase("foo.lambdaProp") - @TestCase("Foo.staticMethod") - @TestCase("Foo.staticLambdaProp") - @TestCase("thisFunc") - @TestCase("thisLambda") - @TestCase("function(this: Foo, s: string) { return s; }") - @TestCase("foo.method", "string | ((s: string) => string)") - @TestCase("foo.method", "T") - @Test("Invalid function argument") - public invalidFunctionArgument(func: string, funcType?: string): void { - if (!funcType) { - funcType = "(s: string) => string"; + const result = foo.bar ||= true; + return { result, foo }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++o.p", + "o.p++", + "--o.p", + "o.p--", + "o.p += a[0]", + "o.p -= a[0]", + "o.p *= a[0]", + "a[0] /= o.p", + "a[0] %= o.p", + "a[0] **= o.p", + "o.p |= a[0]", + "o.p &= a[0]", + "o.p ^= a[0]", + "o.p <<= a[0]", + "o.p >>>= a[0]", +])("Operator assignment to simple property statements (%p)", statement => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + ${statement}; + return { o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++o.p.d", + "o.p.d++", + "--o.p.d", + "o.p.d--", + "o.p.d += a[0][0]", + "o.p.d -= a[0][0]", + "o.p.d *= a[0][0]", + "a[0][0] /= o.p.d", + "a[0][0] %= o.p.d", + "a[0][0] **= o.p.d", + "o.p.d |= a[0][0]", + "o.p.d &= a[0][0]", + "o.p.d ^= a[0][0]", + "o.p.d <<= a[0][0]", + "o.p.d >>>= a[0][0]", +])("Operator assignment to deep property statements (%p)", statement => { + util.testFunction` + let o = { p: { d: 3 } }; + let a = [[6,11], [7,13]]; + ${statement}; + return { o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++of().p", + "of().p++", + "--of().p", + "of().p--", + "of().p += af()[i()]", + "of().p -= af()[i()]", + "of().p *= af()[i()]", + "af()[i()] /= of().p", + "af()[i()] %= of().p", + "af()[i()] **= of().p", + "of().p |= af()[i()]", + "of().p &= af()[i()]", + "of().p ^= af()[i()]", + "of().p <<= af()[i()]", + "of().p >>>= af()[i()]", +])("Operator assignment to complex property statements (%p)", statement => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + function of() { return o; } + function af() { return a; } + function i() { return 0; } + ${statement}; + return { o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++of().p.d", + "of().p.d++", + "--of().p.d", + "of().p.d--", + "of().p.d += af()[i()][i()]", + "of().p.d -= af()[i()][i()]", + "of().p.d *= af()[i()][i()]", + "af()[i()][i()] /= of().p.d", + "af()[i()][i()] %= of().p.d", + "af()[i()][i()] **= of().p.d", + "of().p.d |= af()[i()][i()]", + "of().p.d &= af()[i()][i()]", + "of().p.d ^= af()[i()][i()]", + "of().p.d <<= af()[i()][i()]", + "of().p.d >>>= af()[i()][i()]", +])("Operator assignment to complex deep property statements (%p)", statement => { + util.testFunction` + let o = { p: { d: 3 } }; + let a = [[7, 6], [11, 13]]; + function of() { return o; } + function af() { return a; } + let _i = 0; + function i() { return _i++; } + ${statement}; + return { o, a, _i }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++x", + "x++", + "--x", + "x--", + "x += y", + "x -= y", + "x *= y", + "y /= x", + "y %= x", + "y **= x", + "x |= y", + "x &= y", + "x ^= y", + "x <<= y", + "x >>>= y", + "x + (y += 7)", + "x + (y += 7)", + "x++ + (y += 7)", +])("Operator assignment expressions (%p)", expression => { + util.testFunction` + let x = 3; + let y = 6; + const r = ${expression}; + return { r, x, y }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++o.p", + "o.p++", + "--o.p", + "o.p--", + "o.p += a[0]", + "o.p -= a[0]", + "o.p *= a[0]", + "a[0] /= o.p", + "a[0] %= o.p", + "a[0] **= o.p", + "o.p |= a[0]", + "o.p &= a[0]", + "o.p ^= a[0]", + "o.p <<= a[0]", + "o.p >>>= a[0]", + "o.p + (a[0] += 7)", + "o.p += (a[0] += 7)", + "o.p++ + (a[0] += 7)", +])("Operator assignment to simple property expressions (%p)", expression => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + const r = ${expression}; + return { r, o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++of().p", + "of().p++", + "--of().p", + "of().p--", + "of().p += af()[i()]", + "of().p -= af()[i()]", + "of().p *= af()[i()]", + "af()[i()] /= of().p", + "af()[i()] %= of().p", + "af()[i()] **= of().p", + "of().p |= af()[i()]", + "of().p &= af()[i()]", + "of().p ^= af()[i()]", + "of().p <<= af()[i()]", + "of().p >>>= af()[i()]", + "of().p + (af()[i()] += 7)", + "of().p += (af()[i()] += 7)", + "of().p++ + (af()[i()] += 7)", +])("Operator assignment to complex property expressions (%p)", expression => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + function of() { return o; } + function af() { return a; } + function i() { return 0; } + const r = ${expression}; + return { r, o, a }; + `.expectToMatchJsResult(); +}); + +test("local variable declaration referencing self indirectly", () => { + util.testFunction` + let cb: () => void; + + function foo(newCb: () => void) { + cb = newCb; + return "foo"; } - const code = `${AssignmentTests.funcAssignTestCode} - declare function takesFunc string)>(fn: ${funcType}); - takesFunc(${func});`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from method to function \"fn\". To fix, wrap the method in an arrow function."); - } - @TestCase("<(s: string) => string>foo.method") - @TestCase("foo.method as ((s: string) => string)") - @Test("Invalid function argument cast") - public invalidFunctionArgumentCast(func: string): void { - const code = `${AssignmentTests.funcAssignTestCode} - declare function takesFunc string)>(fn: (s: string) => string); - takesFunc(${func});`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from method to function. To fix, wrap the method in an arrow function."); - } + let bar = foo(() => { + bar = "bar"; + }); - @TestCase("foo.method") - @TestCase("foo.lambdaProp") - @TestCase("Foo.staticMethod") - @TestCase("Foo.staticLambdaProp") - @TestCase("thisFunc") - @TestCase("thisLambda") - @TestCase("function(this: Foo, s: string) { return s; }") - @TestCase("foo.method", "string | ((s: string) => string)") - @TestCase("foo.method", "T") - @TestCase("<(s: string) => string>foo.method") - @TestCase("foo.method as ((s: string) => string)") - @Test("Invalid function return") - public invalidFunctionReturn(func: string, funcType?: string): void { - if (!funcType) { - funcType = "(s: string) => string"; - } - const code = `${AssignmentTests.funcAssignTestCode} - function returnsFunc string)>(): ${funcType} { - return ${func}; - }`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from method to function. To fix, wrap the method in an arrow function."); - } + cb(); + return bar; + `.expectToMatchJsResult(); +}); - @TestCase("foo.method", "func") - @TestCase("foo.method", "lambda") - @TestCase("foo.method", "Foo.voidStaticMethod") - @TestCase("foo.method", "Foo.voidStaticLambdaProp") - @TestCase("foo.method", "foo.voidMethod") - @TestCase("foo.method", "foo.voidLambdaProp") - @TestCase("foo.method", "function(this: void, s: string) { return s; }") - @TestCase("foo.lambdaProp", "func") - @TestCase("foo.lambdaProp", "lambda") - @TestCase("foo.lambdaProp", "Foo.voidStaticMethod") - @TestCase("foo.lambdaProp", "Foo.voidStaticLambdaProp") - @TestCase("foo.lambdaProp", "foo.voidMethod") - @TestCase("foo.lambdaProp", "foo.voidLambdaProp") - @TestCase("foo.lambdaProp", "function(this: void, s: string) { return s; }") - @TestCase("Foo.staticMethod", "func") - @TestCase("Foo.staticMethod", "lambda") - @TestCase("Foo.staticMethod", "Foo.voidStaticMethod") - @TestCase("Foo.staticMethod", "Foo.voidStaticLambdaProp") - @TestCase("Foo.staticMethod", "foo.voidMethod") - @TestCase("Foo.staticMethod", "foo.voidLambdaProp") - @TestCase("Foo.staticMethod", "function(this: void, s: string) { return s; }") - @TestCase("Foo.staticLambdaProp", "func") - @TestCase("Foo.staticLambdaProp", "lambda") - @TestCase("Foo.staticLambdaProp", "Foo.voidStaticMethod") - @TestCase("Foo.staticLambdaProp", "Foo.voidStaticLambdaProp") - @TestCase("Foo.staticLambdaProp", "foo.voidMethod") - @TestCase("Foo.staticLambdaProp", "foo.voidLambdaProp") - @TestCase("Foo.staticLambdaProp", "function(this: void, s: string) { return s; }") - @TestCase("thisFunc", "func") - @TestCase("thisFunc", "lambda") - @TestCase("thisFunc", "Foo.voidStaticMethod") - @TestCase("thisFunc", "Foo.voidStaticLambdaProp") - @TestCase("thisFunc", "foo.voidMethod") - @TestCase("thisFunc", "foo.voidLambdaProp") - @TestCase("thisFunc", "function(this: void, s: string) { return s; }") - @TestCase("thisLambda", "func") - @TestCase("thisLambda", "lambda") - @TestCase("thisLambda", "Foo.voidStaticMethod") - @TestCase("thisLambda", "Foo.voidStaticLambdaProp") - @TestCase("thisLambda", "foo.voidMethod") - @TestCase("thisLambda", "foo.voidLambdaProp") - @TestCase("thisLambda", "function(this: void, s: string) { return s; }") - @TestCase("foo.method", "(func as string | ((s: string) => string))") - @TestCase("foo.method", "<(this: Foo, s: string) => string>func") - @TestCase("foo.method", "func as ((this: Foo, s: string) => string)") - @Test("Invalid method assignment") - public invalidMethodAssignment(func: string, assignTo: string): void { - const code = `${AssignmentTests.funcAssignTestCode} ${func} = ${assignTo};`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from function to method. To fix, wrap the function in an arrow function or declare" - + " the function with an explicit 'this' parameter."); - } +test("local multiple variable declaration referencing self indirectly", () => { + util.testFunction` + let cb: () => void; - @TestCase("func") - @TestCase("lambda") - @TestCase("Foo.voidStaticMethod") - @TestCase("Foo.voidStaticLambdaProp") - @TestCase("foo.voidMethod") - @TestCase("foo.voidLambdaProp") - @TestCase("function(this: void, s: string) { return s; }") - @TestCase("func", "string | ((this: Foo, s: string) => string)") - @TestCase("func", "T") - @Test("Invalid method argument") - public invalidMethodArgument(func: string, funcType?: string): void { - if (!funcType) { - funcType = "(this: Foo, s: string) => string"; + function foo(newCb: () => void) { + cb = newCb; + return ["a", "foo", "c"]; } - const code = `${AssignmentTests.funcAssignTestCode} - declare function takesMethod string)>(meth: ${funcType}); - takesMethod(${func});`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from function to method \"meth\". To fix, wrap the function in an arrow function " - + "or declare the function with an explicit 'this' parameter."); - } - - @TestCase("<(this: Foo, s: string) => string>func") - @TestCase("func as ((this: Foo, s: string) => string)") - @Test("Invalid method argument cast") - public invalidMethodArgumentCast(func: string): void { - const code = `${AssignmentTests.funcAssignTestCode} - declare function takesMethod string)>( - meth: (this: Foo, s: string) => string); - takesMethod(${func});`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from function to method. To fix, wrap the function in an arrow function " - + "or declare the function with an explicit 'this' parameter."); - } - @TestCase("func") - @TestCase("lambda") - @TestCase("Foo.voidStaticMethod") - @TestCase("Foo.voidStaticLambdaProp") - @TestCase("foo.voidMethod") - @TestCase("foo.voidLambdaProp") - @TestCase("function(this: void, s: string) { return s; }") - @TestCase("func", "string | ((this: Foo, s: string) => string)") - @TestCase("func", "T") - @TestCase("<(this: Foo, s: string) => string>func") - @TestCase("func as ((this: Foo, s: string) => string)") - @Test("Invalid method return") - public invalidMethodReturn(func: string, funcType?: string): void { - if (!funcType) { - funcType = "(this: Foo, s: string) => string"; - } - const code = `${AssignmentTests.funcAssignTestCode} - function returnsMethod string)>(): ${funcType} { - return ${func}; - }`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from function to method. To fix, wrap the function in an arrow function " - + "or declare the function with an explicit 'this' parameter."); - } - - @Test("Interface method assignment") - public interfaceMethodAssignment(): void { - const code = `class Foo { - method(s: string): string { return s + "+method"; } - lambdaProp: (s: string) => string = s => s + "+lambdaProp"; - } - interface IFoo { - method: (s: string) => string; - lambdaProp(s: string): string; - } - const foo: IFoo = new Foo(); - return foo.method("foo") + "|" + foo.lambdaProp("bar");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo+method|bar+lambdaProp"); - } - - @Test("Valid function tuple assignment") - public validFunctionTupleAssignment(): void { - const code = `interface Func { (s: string): string; } - function getTuple(): [number, Func] { return [1, s => s]; } - let [i, f]: [number, Func] = getTuple(); - return f("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } - - @Test("Invalid function tuple assignment") - public invalidFunctionTupleAssignment(): void { - const code = `interface Func { (s: string): string; } - interface Meth { (this: {}, s: string): string; } - declare function getTuple(): [number, Meth]; - let [i, f]: [number, Func] = getTuple();`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from method to function. To fix, wrap the method in an arrow function."); - } - - @Test("Valid method tuple assignment") - public validMethodTupleAssignment(): void { - const code = `interface Foo { method(s: string): string; } - interface Meth { (this: Foo, s: string): string; } - let meth: Meth = s => s; - function getTuple(): [number, Meth] { return [1, meth]; } - let [i, f]: [number, Meth] = getTuple(); - let foo: Foo = {method: f}; - return foo.method("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } - - @Test("Invalid method tuple assignment") - public invalidMethodTupleAssignment(): void { - const code = `interface Func { (s: string): string; } - interface Meth { (this: {}, s: string): string; } - declare function getTuple(): [number, Func]; - let [i, f]: [number, Meth] = getTuple();`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from function to method. To fix, wrap the function in an arrow function or declare" - + " the function with an explicit 'this' parameter."); - } - - @Test("Valid interface method assignment") - public validInterfaceMethodAssignment(): void { - const code = `interface A { fn(this: void, s: string): string; } - interface B { fn(this: void, s: string): string; } - const a: A = { fn(this: void, s) { return s; } }; - const b: B = a; - return b.fn("foo");`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } - - @Test("Invalid interface method assignment") - public invalidInterfaceMethodAssignment(): void { - const code = `interface A { fn(s: string): string; } - interface B { fn(this: void, s: string): string; } - declare const a: A; - const b: B = a;`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported conversion from method to function \"fn\". To fix, wrap the method in an arrow function."); - } - - @TestCase("(s: string) => string", ["foo"], "foobar") - @TestCase("{(s: string): string}", ["foo"], "foobar") - @TestCase("(s1: string, s2: string) => string", ["foo", "baz"], "foobaz") - @TestCase("{(s1: string, s2: string): string}", ["foo", "baz"], "foobaz") - @Test("Valid function overload assignment") - public validFunctionOverloadAssignment(assignType: string, args: string[], expectResult: string): void { - const code = `interface O { - (s1: string, s2: string): string; - (s: string): string; - } - const o: O = (s1: string, s2?: string) => s1 + (s2 || "bar"); - let f: ${assignType} = o; - return f(${args.map(a => "\"" + a + "\"").join(", ")});`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @TestCase("(s: string) => string") - @TestCase("(s1: string, s2: string) => string") - @TestCase("{(s: string): string}") - @TestCase("{(this: {}, s1: string, s2: string): string}") - @Test("Invalid function overload assignment") - public invalidFunctionOverloadAssignment(assignType: string): void { - const code = `interface O { - (this: {}, s1: string, s2: string): string; - (s: string): string; - } - declare const o: O; - let f: ${assignType} = o;`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Unsupported assignment of mixed function/method overload. " - + "Overloads should either be all functions or all methods, but not both."); - } - - @TestCase("s => s") - @TestCase("(s => s)") - @TestCase("function(s) { return s; }") - @TestCase("(function(s) { return s; })") - @Test("Function expression type inference in class") - public functionExpressionTypeInferenceInClass(funcExp: string): void { - const code = - `class Foo { - func: (this: void, s: string) => string = ${funcExp}; - method: (s: string) => string = ${funcExp}; - static staticFunc: (this: void, s: string) => string = ${funcExp}; - static staticMethod: (s: string) => string = ${funcExp}; + let [a, bar, c] = foo(() => { + bar = "bar"; + }); + + cb(); + return bar; + `.expectToMatchJsResult(); +}); + +describe.each(["x &&= y", "x ||= y"])("boolean compound assignment (%p)", assignment => { + const booleanCases = [ + [false, false], + [false, true], + [true, false], + [true, true], + ]; + test.each(booleanCases)("matches JS", (x, y) => { + util.testFunction` + let x = ${x}; + let y = ${y}; + ${assignment}; + return x; + `.expectToMatchJsResult(); + }); +}); + +test.each([undefined, 3])("nullish coalescing compound assignment", initialValue => { + util.testFunction` + let x: number = ${util.formatCode(initialValue)}; + x ??= 5; + return x; + `.expectToMatchJsResult(); +}); + +test("nullish coalescing compound assignment lhs false", () => { + util.testFunction` + let x = false; + x ??= true; + return x; + `.expectToMatchJsResult(); +}); + +test("nullish coalescing compound assignment side effect not evaluated", () => { + util.testFunction` + let x = 3; + let y = 10; + x ??= (y += 5); + return [x, y]; + `.expectToMatchJsResult(); +}); + +test.each([ + { operator: "||=", initialValue: true }, + { operator: "&&=", initialValue: false }, + { operator: "??=", initialValue: false }, +])("compound assignment short-circuits and does not call setter", ({ operator, initialValue }) => { + /* + In JS if the rhs does not affect the resulting value, the setter is NOT called: + * x.y ||= z is translated to x.y || (x.y = z). + * x.y &&= z is translated to x.y && (x.y = z). + * x.y ||= z is translated to x.y !== undefined && (x.y = z). + + Test if setter in Lua is called same nr of times as in JS. + */ + util.testModule` + export let setterCalled = 0; + + class MyClass { + + get prop(): any { + return ${initialValue}; } - const foo = new Foo(); - return foo.func("a") + foo.method("b") + Foo.staticFunc("c") + Foo.staticMethod("d");`; - Expect(util.transpileAndExecute(code)).toBe("abcd"); - } - @TestCase("const foo: Foo", "s => s") - @TestCase("const foo: Foo", "(s => s)") - @TestCase("const foo: Foo", "function(s) { return s; }") - @TestCase("const foo: Foo", "(function(s) { return s; })") - @TestCase("let foo: Foo; foo", "s => s") - @TestCase("let foo: Foo; foo", "(s => s)") - @TestCase("let foo: Foo; foo", "function(s) { return s; }") - @TestCase("let foo: Foo; foo", "(function(s) { return s; })") - @Test("Function expression type inference in object literal") - public functionExpressionTypeInferenceInObjectLiteral(assignTo: string, funcExp: string): void { - const code = - `interface Foo { - func(this: void, s: string): string; - method(this: this, s: string): string; + set prop(value: any) { + setterCalled++; } - ${assignTo} = {func: ${funcExp}, method: ${funcExp}}; - return foo.method("foo") + foo.func("bar");`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } + } - @TestCase("const foo: Foo", "s => s") - @TestCase("const foo: Foo", "(s => s)") - @TestCase("const foo: Foo", "function(s) { return s; }") - @TestCase("const foo: Foo", "(function(s) { return s; })") - @TestCase("let foo: Foo; foo", "s => s") - @TestCase("let foo: Foo; foo", "(s => s)") - @TestCase("let foo: Foo; foo", "function(s) { return s; }") - @TestCase("let foo: Foo; foo", "(function(s) { return s; })") - @Test("Function expression type inference in object literal (generic key)") - public functionExpressionTypeInferenceInObjectLiteralGenericKey(assignTo: string, funcExp: string): void { - const code = - `interface Foo { - [f: string]: (this: void, s: string) => string; + const inst = new MyClass(); + inst.prop ${operator} 8; + `.expectToMatchJsResult(); +}); + +test.each([ + { operator: "||=", initialValue: true }, + { operator: "&&=", initialValue: false }, + { operator: "??=", initialValue: false }, +])("compound assignment short-circuits and does not call setter as expression", ({ operator, initialValue }) => { + /* + In JS if the rhs does not affect the resulting value, the setter is NOT called: + * x.y ||= z is translated to x.y || (x.y = z). + * x.y &&= z is translated to x.y && (x.y = z). + * x.y ||= z is translated to x.y !== undefined && (x.y = z). + + Test if setter in Lua is called same nr of times as in JS. + */ + util.testModule` + export let setterCalled = 0; + + class MyClass { + + get prop(): any { + return ${initialValue}; } - ${assignTo} = {func: ${funcExp}}; - return foo.func("foo");`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } - @TestCase("const funcs: [Func, Method]", "funcs[0]", "funcs[1]", "s => s") - @TestCase("const funcs: [Func, Method]", "funcs[0]", "funcs[1]", "(s => s)") - @TestCase("const funcs: [Func, Method]", "funcs[0]", "funcs[1]", "function(s) { return s; }") - @TestCase("const funcs: [Func, Method]", "funcs[0]", "funcs[1]", "(function(s) { return s; })") - @TestCase("let funcs: [Func, Method]; funcs", "funcs[0]", "funcs[1]", "s => s") - @TestCase("let funcs: [Func, Method]; funcs", "funcs[0]", "funcs[1]", "(s => s)") - @TestCase("let funcs: [Func, Method]; funcs", "funcs[0]", "funcs[1]", "function(s) { return s; }") - @TestCase("let funcs: [Func, Method]; funcs", "funcs[0]", "funcs[1]", "(function(s) { return s; })") - @TestCase("const [func, meth]: [Func, Method]", "func", "meth", "s => s") - @TestCase("const [func, meth]: [Func, Method]", "func", "meth", "(s => s)") - @TestCase("const [func, meth]: [Func, Method]", "func", "meth", "function(s) { return s; }") - @TestCase("const [func, meth]: [Func, Method]", "func", "meth", "(function(s) { return s; })") - @TestCase("let func: Func; let meth: Method; [func, meth]", "func", "meth", "s => s") - @TestCase("let func: Func; let meth: Method; [func, meth]", "func", "meth", "(s => s)") - @TestCase("let func: Func; let meth: Method; [func, meth]", "func", "meth", "function(s) { return s; }") - @TestCase("let func: Func; let meth: Method; [func, meth]", "func", "meth", "(function(s) { return s; })") - @Test("Function expression type inference in tuple") - public functionExpressionTypeInferenceInTuple( - assignTo: string, - func: string, - method: string, - funcExp: string - ): void - { - const code = - `interface Foo { - method(s: string): string; - } - interface Func { - (this: void, s: string): string; - } - interface Method { - (this: Foo, s: string): string; + set prop(value: any) { + setterCalled++; } - ${assignTo} = [${funcExp}, ${funcExp}]; - const foo: Foo = {method: ${method}}; - return foo.method("foo") + ${func}("bar");`; - Expect(util.transpileAndExecute(code)).toBe("foobar"); - } + } - @TestCase("const meths: Method[]", "meths[0]", "s => s") - @TestCase("const meths: Method[]", "meths[0]", "(s => s)") - @TestCase("const meths: Method[]", "meths[0]", "function(s) { return s; }") - @TestCase("const meths: Method[]", "meths[0]", "(function(s) { return s; })") - @TestCase("let meths: Method[]; meths", "meths[0]", "s => s") - @TestCase("let meths: Method[]; meths", "meths[0]", "(s => s)") - @TestCase("let meths: Method[]; meths", "meths[0]", "function(s) { return s; }") - @TestCase("let meths: Method[]; meths", "meths[0]", "(function(s) { return s; })") - @TestCase("const [meth]: Method[]", "meth", "s => s") - @TestCase("const [meth]: Method[]", "meth", "(s => s)") - @TestCase("const [meth]: Method[]", "meth", "function(s) { return s; }") - @TestCase("const [meth]: Method[]", "meth", "(function(s) { return s; })") - @TestCase("let meth: Method; [meth]", "meth", "s => s") - @TestCase("let meth: Method; [meth]", "meth", "(s => s)") - @TestCase("let meth: Method; [meth]", "meth", "function(s) { return s; }") - @TestCase("let meth: Method; [meth]", "meth", "(function(s) { return s; })") - @Test("Function expression type inference in array") - public functionExpressionTypeInferenceInArray(assignTo: string, method: string, funcExp: string): void { - const code = - `interface Foo { - method(s: string): string; - } - interface Method { - (this: Foo, s: string): string; - } - ${assignTo} = [${funcExp}]; - const foo: Foo = {method: ${method}}; - return foo.method("foo");`; - Expect(util.transpileAndExecute(code)).toBe("foo"); - } + const inst = new MyClass(); + export const result = (inst.prop ${operator} 8); + `.expectToMatchJsResult(); +}); + +test.each([ + { operator: "+=", initialValue: 3 }, + { operator: "-=", initialValue: 10 }, + { operator: "*=", initialValue: 4 }, + { operator: "/=", initialValue: 20 }, + { operator: "||=", initialValue: false }, + { operator: "&&=", initialValue: true }, + { operator: "??=", initialValue: undefined }, +])("compound assignment side effects", ({ operator, initialValue }) => { + // Test if when assigning to something with potential side effects, they are only evaluated once. + util.testFunction` + const obj: { prop: any} = { prop: ${initialValue} }; + + let objGot = 0; + function getObj() { + objGot++; + return obj; + } - @Test("String table access") - public stringTableAccess(assignType: string): void { - const code = `const dict : {[key:string]:any} = {}; - dict["a b"] = 3; - return dict["a b"];`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(3); - } + getObj().prop ${operator} 4; + + return [obj, objGot]; + `.expectToMatchJsResult(); +}); -} +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1412 +test("invalid assignment to call expression (#1412)", () => { + util.testFunction` + function foo(): number { + return 3; + } + foo() += 4; + `.expectNoTranspileException(); +}); diff --git a/test/unit/bindingpatterns.spec.ts b/test/unit/bindingpatterns.spec.ts deleted file mode 100644 index ad0e18638..000000000 --- a/test/unit/bindingpatterns.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Expect, Test, TestCase, TestCases } from "alsatian"; - -import * as util from "../src/util"; - -const testCases = [ - ["{x}", "{x: true}", "x"], - ["[x, y]", "[false, true]", "y"], - ["{x: [y, z]}", "{x: [false, true]}", "z"], - ["{x: [, z]}", "{x: [false, true]}", "z"], - ["{x: [{y}]}", "{x: [{y: true}]}", "y"], - ["[[y, z]]", "[[false, true]]", "z"], - ["{x, y}", "{x: false, y: true}", "y"], - ["{x: foo, y}", "{x: true, y: false}", "foo"], - ["{x: foo, y: bar}", "{x: false, y: true}", "bar"], - ["{x: {x, y}, z}", "{x: {x: true, y: false}, z: false}", "x"], - ["{x: {x, y}, z}", "{x: {x: false, y: true}, z: false}", "y"], - ["{x: {x, y}, z}", "{x: {x: false, y: false}, z: true}", "z"], -]; - -const testCasesDefault = [ - ["{x = true}", "{}", "x"], - ["{x, y = true}", "{x: false}", "y"], -]; - -export class BindingPatternTests { - - @TestCase("{x, y}, z", "{x: false, y: false}, true", "z") - @TestCase("{x, y}, {z}", "{x: false, y: false}, {z: true}", "z") - @TestCases(testCases) - @TestCases(testCasesDefault) - @Test("Object bindings in functions") - public tesBindingPatternParameters( - bindingString: string, - objectString: string, - returnVariable: string - ): void { - const result = util.transpileAndExecute(` - function test(${bindingString}) { - return ${returnVariable}; - } - return test(${objectString}); - `); - Expect(result).toBe(true); - } - - @TestCases(testCases) - @TestCases(testCasesDefault) - public testBindingPatternDeclarations( - bindingString: string, - objectString: string, - returnVariable: string - ): void { - const result = util.transpileAndExecute(` - let ${bindingString} = ${objectString}; - return ${returnVariable}; - `); - Expect(result).toBe(true); - } - - @TestCases(testCases) - @TestCases(testCasesDefault) - public testBindingPatternExportDeclarations( - bindingString: string, - objectString: string, - returnVariable: string - ): void { - const result = util.transpileExecuteAndReturnExport( - `export const ${bindingString} = ${objectString};`, - returnVariable - ); - Expect(result).toBe(true); - } - - @TestCases(testCases) - @Test("Object bindings with call expressions") - public testBindingPatternCallExpressions( - bindingString: string, - objectString: string, - returnVariable: string - ): void { - const result = util.transpileAndExecute(` - function call() { - return ${objectString}; - } - let ${bindingString} = call(); - return ${returnVariable}; - `); - Expect(result).toBe(true); - } - -} diff --git a/test/unit/builtins/__snapshots__/console.spec.ts.snap b/test/unit/builtins/__snapshots__/console.spec.ts.snap new file mode 100644 index 000000000..2c9d8578c --- /dev/null +++ b/test/unit/builtins/__snapshots__/console.spec.ts.snap @@ -0,0 +1,247 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`console.assert ("console.assert(false)") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert(false) +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message %%s\\", \\"info\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert( + false, + string.format("message %%s", "info") + ) +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message %s\\", \\"info\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert( + false, + string.format("message %s", "info") + ) +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert(false, "message") +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message\\", \\"more\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert(false, "message", "more") +end +return ____exports" +`; + +exports[`console.error ("console.error()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print() +end +return ____exports" +`; + +exports[`console.error ("console.error(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %%s", "there")) +end +return ____exports" +`; + +exports[`console.error ("console.error(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %s", "there")) +end +return ____exports" +`; + +exports[`console.error ("console.error(\\"Hello\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello") +end +return ____exports" +`; + +exports[`console.error ("console.error(\\"Hello\\", \\"There\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello", "There") +end +return ____exports" +`; + +exports[`console.info ("console.info()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print() +end +return ____exports" +`; + +exports[`console.info ("console.info(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %%s", "there")) +end +return ____exports" +`; + +exports[`console.info ("console.info(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %s", "there")) +end +return ____exports" +`; + +exports[`console.info ("console.info(\\"Hello\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello") +end +return ____exports" +`; + +exports[`console.info ("console.info(\\"Hello\\", \\"There\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello", "There") +end +return ____exports" +`; + +exports[`console.log ("console.log()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print() +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %%s", "there")) +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %s", "there")) +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello") +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello\\", \\"There\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello", "There") +end +return ____exports" +`; + +exports[`console.trace ("console.trace()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(debug.traceback()) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(debug.traceback(string.format("Hello %%s", "there"))) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(debug.traceback(string.format("Hello %s", "there"))) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"Hello\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(debug.traceback("Hello", "there")) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"message\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(debug.traceback("message")) +end +return ____exports" +`; + +exports[`console.warn ("console.warn()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print() +end +return ____exports" +`; + +exports[`console.warn ("console.warn(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %%s", "there")) +end +return ____exports" +`; + +exports[`console.warn ("console.warn(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(string.format("Hello %s", "there")) +end +return ____exports" +`; + +exports[`console.warn ("console.warn(\\"Hello\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello") +end +return ____exports" +`; + +exports[`console.warn ("console.warn(\\"Hello\\", \\"There\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print("Hello", "There") +end +return ____exports" +`; diff --git a/test/unit/builtins/__snapshots__/loading.spec.ts.snap b/test/unit/builtins/__snapshots__/loading.spec.ts.snap new file mode 100644 index 000000000..de7fb50b4 --- /dev/null +++ b/test/unit/builtins/__snapshots__/loading.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Unknown builtin property access: code 1`] = ` +"local ____exports = {} +____exports.__result = Math.unknownProperty +return ____exports" +`; + +exports[`Unknown builtin property access: diagnostics 1`] = `"main.ts(1,30): error TSTL: Math.unknownProperty is unsupported."`; diff --git a/test/unit/builtins/array.spec.ts b/test/unit/builtins/array.spec.ts new file mode 100644 index 000000000..dfb6807ec --- /dev/null +++ b/test/unit/builtins/array.spec.ts @@ -0,0 +1,929 @@ +import { + undefinedInArrayLiteral, + unsupportedArrayWithLengthConstructor, +} from "../../../src/transformation/utils/diagnostics"; +import * as util from "../../util"; + +test("omitted expression", () => { + util.testFunction` + const array = [1, , 2]; + return { a: array[0], b: array[1], c: array[2] }; + `.expectToMatchJsResult(); +}); + +describe("access", () => { + test("Array", () => { + util.testFunction` + const array: Array = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("ReadonlyArray", () => { + util.testFunction` + const array: ReadonlyArray = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("array literal", () => { + util.testExpression`[3, 5, 1][1]`.expectToMatchJsResult(); + }); + + test("const array literal", () => { + util.testExpression`([3, 5, 1] as const)[1]`.expectToMatchJsResult(); + }); + + test("tuple", () => { + util.testFunction` + const tuple: [number, number, number] = [3, 5, 1]; + return tuple[1]; + `.expectToMatchJsResult(); + }); + + test("readonly tuple", () => { + util.testFunction` + const tuple: readonly [number, number, number] = [3, 5, 1]; + return tuple[1]; + `.expectToMatchJsResult(); + }); + + test("union", () => { + util.testFunction` + const array: number[] | string[] = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("union with empty tuple", () => { + util.testFunction` + const array: number[] | [] = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("union with tuple", () => { + util.testFunction` + const tuple: number[] | [number, number, number] = [3, 5, 1]; + return tuple[1]; + `.expectToMatchJsResult(); + }); + + test("access in call", () => { + util.testExpression`[() => "foo", () => "bar"][0]()`.expectToMatchJsResult(); + }); + + test("intersection", () => { + util.testFunction` + const array = Object.assign([3, 5, 1], { foo: "bar" }); + return { foo: array.foo, a: array[0], b: array[1], c: array[2] }; + `.expectToMatchJsResult(); + }); + + test("with enum value index", () => { + util.testFunction` + enum TestEnum { + A, + B, + C, + } + + const array = ["a", "b", "c"]; + let index = TestEnum.A; + return array[index]; + `.expectToMatchJsResult(); + }); + + test.each([ + { member: "firstElement()", expected: 3 }, + { member: "name", expected: "array" }, + { member: "length", expected: 1 }, + ])("derived array (.%p)", ({ member, expected }) => { + const luaHeader = ` + local array = { + name = "array", + firstElement = function(self) return self[1] end + } + `; + + util.testModule` + interface CustomArray extends Array { + name: string; + firstElement(): number; + }; + + declare const array: CustomArray; + + array[0] = 3; + export const result = array.${member}; + ` + .setReturnExport("result") + .setLuaHeader(luaHeader) + .expectToEqual(expected); + }); +}); + +describe("array.length", () => { + describe("get", () => { + test("union", () => { + util.testFunction` + const array: number[] | string[] = [3, 5, 1]; + return array.length; + `.expectToMatchJsResult(); + }); + + test("intersection", () => { + util.testFunction` + const array = Object.assign([3, 5, 1], { foo: "bar" }); + return array.length; + `.expectToMatchJsResult(); + }); + + test("tuple", () => { + util.testFunction` + const tuple: [number, number, number] = [3, 5, 1]; + return tuple.length; + `.expectToMatchJsResult(); + }); + }); + + describe("set", () => { + test.each([ + { length: 0, newLength: 0 }, + { length: 1, newLength: 1 }, + { length: 7, newLength: 3 }, + ])("removes extra elements", ({ length, newLength }) => { + util.testFunction` + const array = [1, 2, 3]; + array.length = ${length}; + return array.length; + `.expectToEqual(newLength); + }); + + test.each([0, 1, 7])("returns right-hand side value", length => { + util.testExpression`[1, 2, 3].length = ${length}`.expectToEqual(length); + }); + + test.each([-1, -7, 0.1])("throws on invalid values (%p)", length => { + util.testFunction` + [1, 2, 3].length = ${length}; + `.expectToEqual(new util.ExecutionError(`invalid array length: ${length}`)); + }); + + test.each([NaN, Infinity, -Infinity])("throws on invalid special values (%p)", length => { + // Need to get the actual lua tostring version of inf/nan + // this is platform dependent so we can/should not hardcode it + const luaSpecialValueString = util.testExpression`(${length}).toString()`.getLuaExecutionResult(); + util.testFunction` + [1, 2, 3].length = ${length}; + `.expectToEqual(new util.ExecutionError(`invalid array length: ${luaSpecialValueString}`)); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1395 + test("in compound assignment (#1395)", () => { + util.testFunction` + const arr = [1,2,3,4]; + const returnVal = arr.length -= 2; + return { arr, returnVal }; + `.expectToMatchJsResult(); + }); + + test("as standalone compound assignment (#1395)", () => { + util.testFunction` + const arr = [1,2,3,4]; + arr.length -= 2; + return arr; + `.expectToMatchJsResult(); + }); + + test("in array destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + [array.length] = [0]; + return array.length; + `.expectToEqual(0); + }); + + test("in nested array destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + [[array.length]] = [[0]]; + return array.length; + `.expectToEqual(0); + }); + + test("in object destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + ({ length: array.length } = { length: 0 }); + return array.length; + `.expectToEqual(0); + }); + + test("in nested object destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + ({ obj: { length: array.length } } = { obj: { length: 0 } }); + return array.length; + `.expectToEqual(0); + }); + }); +}); + +describe("delete", () => { + test("deletes element", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + delete array[2]; + return { a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); + + test("returns true when deletion attempt was allowed", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + const success = delete array[2]; + return { success, a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); + + test("returns false when deletion attempt was disallowed", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + Object.defineProperty(array, 2, { configurable: false }); + + let success; + try { + success = delete array[2]; + } catch { + success = "error"; + } + + return { success, a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); +}); + +test("tuple.forEach", () => { + util.testFunction` + const tuple: [number, number, number] = [3, 5, 1]; + let count = 0; + tuple.forEach(value => { + count += value; + }); + return count; + `.expectToMatchJsResult(); +}); + +describe("at", () => { + test("valid index", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + return array.at(2); + `.expectToMatchJsResult(); + }); + + test("invalid index", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + return array.at(10); + `.expectToMatchJsResult(); + }); + + test("valid negative index", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + return array.at(-2); + `.expectToMatchJsResult(); + }); + + test("invalid negative index", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + return array.at(-10); + `.expectToMatchJsResult(); + }); +}); + +test("array.forEach (%p)", () => { + util.testFunction` + const array = [0, 1, 2, 3]; + array.forEach((elem, index) => { + array[index] = array[index] + 1; + }); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [], predicate: "elem > 3" }, + { array: [0, 2, 4, 8], predicate: "elem > 10" }, + { array: [0, 2, 4, 8], predicate: "elem > 7" }, + { array: [0, 2, 4, 8], predicate: "elem == 0" }, + { array: [0, 2, 4, 8], predicate: "elem > 7" }, + { array: [0, 2, 4, 8], predicate: "true" }, + { array: [0, 2, 4, 8], predicate: "false" }, +])("array.find (%p)", ({ array, predicate }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + return array.find((elem, index, arr) => ${predicate} && arr[index] === elem); + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [], searchElement: 3 }, + { array: [0, 2, 4, 8], searchElement: 10 }, + { array: [0, 2, 4, 8], searchElement: 0 }, + { array: [0, 2, 4, 8], searchElement: 8 }, +])("array.findIndex (%p)", ({ array, searchElement }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + return array.findIndex((elem, index, arr) => elem === ${searchElement} && arr[index] === elem); + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x" }, + { array: [0, 1, 2, 3], func: "x => x" }, + { array: [0, 1, 2, 3], func: "x => x*2" }, + { array: [1, 2, 3, 4], func: "x => -x" }, + { array: [0, 1, 2, 3], func: "x => x+2" }, + { array: [0, 1, 2, 3], func: "x => x%2 == 0 ? x + 1 : x - 1" }, +])("array.map (%p)", ({ array, func }) => { + util.testExpression`${util.formatCode(array)}.map(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x < 3" }, + { array: [0, 1, 2, 3], func: "x => x < 0" }, + { array: [0, -1, -2, -3], func: "x => x < 0" }, + { array: [0, 1, 2, 3], func: "() => true" }, + { array: [0, 1, 2, 3], func: "() => false" }, +])("array.filter (%p)", ({ array, func }) => { + util.testExpression`${util.formatCode(array)}.filter(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x > 1" }, + { array: [false, true, false], func: "x => x" }, + { array: [true, true, true], func: "x => x" }, +])("array.every (%p)", ({ array, func }) => { + util.testExpression`${util.formatCode(array)}.every(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x > 1" }, + { array: [false, true, false], func: "x => x" }, + { array: [true, true, true], func: "x => x" }, +])("array.some (%p)", ({ array, func }) => { + util.testExpression`${util.formatCode(array)}.some(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [2, 3, 4, 5], args: [] }, + { array: [], args: [1, 2] }, + { array: [0, 1, 2, 3], args: [1, 2] }, + { array: [0, 1, 2, 3], args: [1, 1] }, + { array: [0, 1, 2, 3], args: [1, -1] }, + { array: [0, 1, 2, 3], args: [-3, -1] }, + { array: [0, 1, 2, 3, 4, 5], args: [1, 3] }, + { array: [0, 1, 2, 3, 4, 5], args: [3] }, +])("array.slice (%p)", ({ array, args }) => { + const argumentString = util.formatCode(...args); + util.testExpression`${util.formatCode(array)}.slice(${argumentString})`.expectToMatchJsResult(); +}); + +test.each([ + // Insert + { array: [], start: 0, deleteCount: 0, newElements: [9, 10, 11] }, + { array: [0, 1, 2, 3], start: 1, deleteCount: 0, newElements: [9, 10, 11] }, + { array: [0, 1, 2, 3], start: 2, deleteCount: 2, newElements: [9, 10, 11] }, + { array: [0, 1, 2, 3], start: 4, deleteCount: 1, newElements: [8, 9] }, + { array: [0, 1, 2, 3], start: 4, deleteCount: 0, newElements: [8, 9] }, + { array: [0, 1, 2, 3], start: -2, deleteCount: 0, newElements: [8, 9] }, + { array: [0, 1, 2, 3], start: -3, deleteCount: 0, newElements: [8, 9] }, + { array: [0, 1, 2, 3, 4, 5], start: 5, deleteCount: 9, newElements: [10, 11] }, + { array: [0, 1, 2, 3, 4, 5], start: 3, deleteCount: 2, newElements: [3, 4, 5] }, + { array: [0, 1, 2, 3, 4, 5, 6, 7, 8], start: 5, deleteCount: 9, newElements: [10, 11] }, + { array: [0, 1, 2, 3, 4, 5, 6, 7, 8], start: 5, deleteCount: undefined, newElements: [10, 11] }, + { array: [0, 1, 2, 3, 4, 5, 6, 7, 8], start: 5, deleteCount: null, newElements: [10, 11] }, + + // Remove + { array: [], start: 1, deleteCount: 1 }, + { array: [0, 1, 2, 3], start: 1, deleteCount: 1 }, + { array: [0, 1, 2, 3], start: 10, deleteCount: 1 }, + { array: [0, 1, 2, 3, 4, 5], start: 2, deleteCount: 2 }, + { array: [0, 1, 2, 3, 4, 5], start: -3, deleteCount: 2 }, + { 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 = [] }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + array.splice(${util.formatCode(start, deleteCount, ...newElements)}); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [0, 1, 2, 3], start: 4 }, + { array: [0, 1, 2, 3, 4, 5], start: 3 }, + { array: [0, 1, 2, 3, 4, 5], start: -3 }, + { array: [0, 1, 2, 3, 4, 5], start: -2 }, +])("array.splice no delete argument", ({ array, start }) => { + util.testExpression`${util.formatCode(array)}.splice(${start})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], args: [[]] }, + { array: [1, 2, 3], args: [[]] }, + { array: [1, 2, 3], args: [[4]] }, + { array: [1, 2, 3], args: [[4, 5]] }, + { array: [1, 2, 3], args: [[4, 5]] }, + { array: [1, 2, 3], args: [4, [5]] }, + { array: [1, 2, 3], args: [4, [5, 6]] }, + { array: [1, 2, 3], args: [4, [5, 6], 7] }, + { array: [1, 2, 3], args: ["test", [5, 6], 7, ["test1", "test2"]] }, + { array: [1, 2, "test"], args: ["test", ["test1", "test2"]] }, +])("array.concat (%p)", ({ array, args }) => { + util.testFunction` + const array: any[] = ${util.formatCode(array)}; + return array.concat(${util.formatCode(...args)}); + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [1, 2, 3], includes: 2 }, + { array: [1, 2, 3, 2, 2], includes: 2 }, + { array: [1, 2, 3], includes: 4 }, + { array: ["a", "b", "c"], includes: "d" }, + { array: [[1], [2], [3]], includes: [2] }, + { array: [1, [2], 3], includes: 2 }, +])("array.includes (%p)", ({ array, includes }) => { + util.testExpressionTemplate`${array}.includes(${includes})`.expectToMatchJsResult(); +}); + +test("array.includes reference", () => { + util.testFunction` + const inst = [2]; + return [[1], [3], inst].includes(inst); + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [1, 2, 3], includes: 2, fromIndex: 0 }, + { array: [1, 2, 3], includes: 2, fromIndex: 1 }, + { array: [1, 2, 3], includes: 2, fromIndex: 2 }, + { array: [1, 2, 3], includes: 2, fromIndex: 3 }, + { array: [1, 2, 3], includes: 2, fromIndex: -1 }, + { array: [1, 2, 3], includes: 2, fromIndex: -2 }, + { array: [1, 2, 3], includes: 2, fromIndex: -3 }, + { array: [1, 2, 3], includes: 2, fromIndex: -4 }, +])("array.includes with fromIndex (%p)", ({ array, includes, fromIndex }) => { + util.testExpressionTemplate`${array}.includes(${includes}, ${fromIndex})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [] }, + { array: ["test1"] }, + { array: ["test1", "test2"] }, + { array: ["test1", "test2"], separator: ";" }, + { array: ["test1", "test2"], separator: "" }, + { array: [1, "2"] }, +])("array.join (%p)", ({ array, separator }) => { + util.testExpression`${util.formatCode(array)}.join(${util.formatCode(separator)})`.expectToMatchJsResult(); +}); + +test('array.join (1, "2", {})', () => { + const result = util.testExpression`[1, "2", {}].join()`.getLuaExecutionResult(); + expect(result).toMatch(/^1,2,table: 0x[\da-f]+$/); +}); + +test('array.join (1, "2", Symbol("foo"))', () => { + util.testExpression`[1, "2", Symbol("foo")].join(", ")`.expectToEqual("1, 2, Symbol(foo)"); +}); + +test("array.join without separator argument", () => { + util.testExpression`["test1", "test2"].join()`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], args: ["test1"] }, + { array: ["test1"], args: ["test1"] }, + { array: ["test1", "test2"], args: ["test2"] }, + { array: ["test1", "test2", "test3"], args: ["test3", 1] }, + { array: ["test1", "test2", "test3"], args: ["test1", 2] }, + { array: ["test1", "test2", "test3"], args: ["test1", -2] }, + { array: ["test1", "test2", "test3"], args: ["test1", 12] }, +])("array.indexOf (%p)", ({ array, args }) => { + util.testExpression`${util.formatCode(array)}.indexOf(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + { args: "1" }, + { args: "1, 2, 3" }, + { args: "...[1, 2, 3]" }, + { args: "1, 2, ...[3, 4]" }, + { args: "1, 2, ...[3, 4], 5" }, +])("array.push (%p)", ({ args }) => { + util.testFunction` + const array = [0]; + const value = array.push(${args}); + return { array, value }; + `.expectToMatchJsResult(); +}); + +test("array.push(...)", () => { + util.testFunction` + const array = [0]; + function push(...args: any[]) { + return array.push(...args); + } + const value = push(1, 2, 3); + return { array, value }; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1256 +test.each(["[1, 2, 3]", "undefined"])("array push on optional array", value => { + util.testFunction` + const arr = ${value} as number[] | undefined; + arr?.push(4); + return arr + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [1, 2, 3], expected: [3, 2] }, + { array: [1, 2, 3, null], expected: [3, 2] }, +])("array.pop (%p)", ({ array, expected }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + const value = array.pop(); + return [value, array.length]; + `.expectToEqual(expected); +}); + +test.each([{ array: [1, 2, 3] }, { array: [1, 2, 3, 4] }, { array: [1] }, { array: [] }])( + "array.reverse (%p)", + ({ array }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + array.reverse(); + return array; + `.expectToMatchJsResult(); + } +); + +test.each([{ array: [1, 2, 3] }, { array: [1] }, { array: [] }])("array.shift (%p)", ({ array }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + const value = array.shift(); + return { array, value }; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [3, 4, 5], args: [1, 2] }, + { array: [], args: [] }, + { array: [1], args: [] }, + { array: [], args: [1] }, +])("array.unshift (%p)", ({ array, args }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + const value = array.unshift(${util.formatCode(...args)}); + return { array, value }; + `.expectToMatchJsResult(); +}); + +test.each([{ array: [4, 5, 3, 2, 1] }, { array: [1] }, { array: [] }])("array.sort (%p)", ({ array }) => { + util.testFunctionTemplate` + const array = ${array}; + array.sort(); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [1, 2, 3, 4, 5], compare: (a: number, b: number) => a - b }, + { array: ["4", "5", "3", "2", "1"], compare: (a: string, b: string) => Number(a) - Number(b) }, + { array: ["4", "5", "3", "2", "1"], compare: (a: string, b: string) => Number(b) - Number(a) }, +])("array.sort with compare function (%p)", ({ array, compare }) => { + util.testFunctionTemplate` + const array = ${array}; + array.sort(${compare}); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [[]] }, + { array: [{ a: 1 }, { a: 2 }, { a: 3 }] }, + { array: [1, [2, 3], 4] }, + { array: [1, [2, 3], 4], depth: 0 }, + { array: [1, [[2], [3]], 4] }, + { array: [1, [[[2], [3]]], 4], depth: Infinity }, +])("array.flat (%p)", ({ array, depth }) => { + util.testExpressionTemplate`${array}.flat(${depth})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [[]], map: (v: T) => v }, + { array: [1, 2, 3], map: (v: number) => ({ a: v * 2 }) }, + { array: [1, [2, 3], [4]], map: (value: T) => value }, + { array: [1, 2, 3], map: (v: number) => v * 2 }, + { array: [1, 2, 3], map: (v: number) => [v, v * 2] }, + { array: [1, 2, 3], map: (v: number) => [v, [v]] }, + { array: [1, 2, 3], map: (v: number, i: number) => [v * 2 * i] }, +])("array.flatMap (%p)", ({ array, map }) => { + util.testExpressionTemplate`${array}.flatMap(${map})`.expectToMatchJsResult(); +}); + +describe.each(["reduce", "reduceRight"])("array.%s", reduce => { + test.each<[[(total: number, currentItem: number, index: number, array: number[]) => number, number?]]>([ + [[(total, currentItem) => total + currentItem]], + [[(total, currentItem) => total * currentItem]], + [[(total, currentItem) => total + currentItem, 10]], + [[(total, currentItem) => total * currentItem, 10]], + [[(total, _, index, array) => total + array[index]]], + [[(a, b) => a + b]], + ])("usage (%p)", args => { + util.testExpression`[1, 3, 5, 7].${reduce}(${util.formatCode(...args)})`.expectToMatchJsResult(); + }); + + test("empty undefined initial", () => { + util.testExpression`[].${reduce}(() => {}, undefined)`.expectToMatchJsResult(); + }); + + test("empty no initial", () => { + util.testExpression`[].${reduce}(() => {})`.expectToMatchJsResult(true); + }); + + test("undefined returning callback", () => { + util.testFunction` + const calls: Array<{ a: void, b: string }> = []; + ["a", "b"].${reduce}((a, b) => { calls.push({ a, b }) }, undefined); + return calls; + `.expectToMatchJsResult(); + }); +}); + +test.each([{ array: [] }, { array: ["a", "b", "c"] }, { array: [{ foo: "foo" }, { bar: "bar" }] }])( + "array.entries (%p)", + ({ array }) => { + util.testFunction` + const array = ${util.formatCode(array)}; + const result = []; + for (const [i, v] of array.entries()) { + result.push([i, v]); + } + return result; + `.expectToMatchJsResult(); + } +); + +test("array.entries indirect use", () => { + util.testFunction` + const entries = ["a", "b", "c"].entries(); + const result = []; + for (const [i, v] of entries) { + result.push([i, v]); + } + return result; + `.expectToMatchJsResult(); +}); + +test("array.entries destructured", () => { + util.testExpression`[...["a", "b", "c"].entries()]`.expectToMatchJsResult(); +}); + +const genericChecks = [ + "function generic(array: T)", + "function generic(array: T)", + "type ArrayType = number[]; function generic(array: T)", + "function generic(array: T & {})", + "function generic(array: T)", +]; + +test.each(genericChecks)("array constrained generic foreach (%p)", signature => { + util.testFunction` + ${signature}: number { + let sum = 0; + array.forEach(item => { + if (typeof item === "number") { + sum += item; + } + }); + return sum; + } + return generic([1, 2, 3]); + `.expectToMatchJsResult(); +}); + +test.each(genericChecks)("array constrained generic length (%p)", signature => { + util.testFunction` + ${signature}: number { + return array.length; + } + return generic([1, 2, 3]); + `.expectToMatchJsResult(); +}); + +test.each(["[]", '"hello"', "42", "[1, 2, 3]", '{ a: "foo", b: "bar" }'])( + "Array.isArray matches JavaScript (%p)", + valueString => { + util.testExpression`Array.isArray(${valueString})`.expectToMatchJsResult(); + } +); + +test("Array.isArray returns true for empty objects", () => { + // Important edge case we cannot handle correctly due to [] and {} + // being identical in Lua. We assume [] is more common than Array.isArray({}), + // so it is more important to handle [] right, sacrificing the result for {}. + // See discussion: https://github.com/TypeScriptToLua/TypeScriptToLua/pull/737 + util.testExpression`Array.isArray({})`.expectToEqual(true); +}); + +test.each([ + "[1, 2, 3]", + "(new Set([1, 2, 3])).values()", + "[1, 2, 3], value => value * 2", + "{ length: 3 }, (_, index) => index + 1", +])("Array.from(%p)", valueString => { + util.testExpression`Array.from(${valueString})`.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1576 +test("array.from with non-array iterable (#1576)", () => { + util.testFunction` + const map = new Map().set(1, 2); + return Array.from(map, ([v,k]) => ({k,v})); + `.expectToMatchJsResult(); +}); + +// Array.of +test.each(["1, 2, 3", "", "...[1, 2, 3], 4, 5, 6"])("Array.of(%p)", valueString => { + util.testExpression`Array.of(${valueString})`.expectToMatchJsResult(); +}); + +// Test fix for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/738 +test("array.prototype.concat issue #738", () => { + util.testExpression`([] as any[]).concat(13, 323, {x: 3}, [2, 3])`.expectToMatchJsResult(); +}); + +test.each(["[1, undefined, 3]", "[1, null, 3]", "[1, undefined, 2, null]"])( + "not allowed to use null or undefined in array literals (%p)", + literal => { + util.testExpression(literal).expectToHaveDiagnostics([undefinedInArrayLiteral.code]); + } +); + +test("not allowed to use null or undefined in array literals ([undefined, null, 1])", () => { + util.testExpression`[undefined, null, 1]`.expectToHaveDiagnostics([ + undefinedInArrayLiteral.code, + undefinedInArrayLiteral.code, + ]); +}); + +test.each([ + "[]", + "[1, 2, undefined]", + "[1, 2, null]", + "[1, undefined, undefined, null]", + "[undefined]", + "[undefined, null]", +])("trailing undefined or null are allowed in array literal (%p)", literal => { + util.testExpression(literal).expectToHaveNoDiagnostics(); +}); + +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(["[1,2,3]", "[1,2,3,4,5,6]"])("Fills starting from start parameter (%p)", arr => { + util.testExpression`${arr}.fill(5, 3)`.expectToMatchJsResult(); + }); + + test("handles negative start parameter", () => { + util.testExpression`[1,2,3,4,5,6,7].fill(8, -3)`.expectToMatchJsResult(); + }); + + test("handles negative end parameter", () => { + util.testExpression`[1,2,3,4,5,6,7].fill(8, -5, -2)`.expectToMatchJsResult(); + }); + + test("Fills starting from start parameter, up to ending parameter", () => { + util.testExpression`[1,2,3,4,5,6,7,8].fill(5, 2, 6)`.expectToMatchJsResult(); + }); + + // NOTE: This is different from the default ECMAScript specification for the behavior, but for Lua this is much more useful + test("Extends size of the array if ending size is larger than array", () => { + util.testExpression`[1,2,3].fill(5, 0, 6)`.expectToEqual([5, 5, 5, 5, 5, 5]); + }); +}); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["[1, 2, 3]", "undefined"])("prototype call on nullable array (%p)", value => { + util.testFunction` + function find(arr?: number[]) { + return arr?.indexOf(2); + } + return find(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); + +describe("copying array methods", () => { + test("toReversed", () => { + util.testFunction` + const original = [1,2,3,4,5]; + const reversed = original.toReversed(); + + return {original, reversed}; + `.expectToEqual({ + original: [1, 2, 3, 4, 5], + reversed: [5, 4, 3, 2, 1], + }); + }); + + test("toSorted", () => { + util.testFunction` + const original = [5,2,1,4,3]; + const sorted = original.toSorted(); + + return {original, sorted}; + `.expectToEqual({ + original: [5, 2, 1, 4, 3], + sorted: [1, 2, 3, 4, 5], + }); + }); + + test("toSpliced", () => { + util.testFunction` + const original = [1,2,3,4,5]; + const spliced = original.toSpliced(2, 2, 10, 11); + + return {original, spliced}; + `.expectToEqual({ + original: [1, 2, 3, 4, 5], + spliced: [1, 2, 10, 11, 5], + }); + }); + + test("with", () => { + util.testFunction` + const original = [1,2,3,4,5]; + const updated = original.with(2, 10); + + return {original, updated}; + `.expectToEqual({ + original: [1, 2, 3, 4, 5], + updated: [1, 2, 10, 4, 5], + }); + }); +}); + +// 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 new file mode 100644 index 000000000..45adb4518 --- /dev/null +++ b/test/unit/builtins/async-await.spec.ts @@ -0,0 +1,818 @@ +import { ModuleKind, ScriptTarget } from "typescript"; +import { LuaTarget } from "../../../src"; +import { unsupportedForTargetButOverrideAvailable } from "../../../src/transformation/utils/diagnostics"; +import { awaitMustBeInAsyncFunction } from "../../../src/transformation/utils/diagnostics"; +import * as util from "../../util"; + +const promiseTestLib = ` +// Some logging utility, useful for asserting orders of operations + +const allLogs: any[] = []; +function log(...values: any[]) { + allLogs.push(...values); +} + +// Create a promise and store its resolve and reject functions, useful for creating pending promises + +function defer() { + let resolve: (data: any) => void = () => {}; + let reject: (reason: string) => void = () => {}; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}`; + +test("high amount of chained awaits doesn't cause stack overflow", () => { + util.testFunction` + let result = "not executed"; + + async function delay() { + return; + } + + async function loop() { + try { + for (let i = 0; i < 500000; ++i) { + await delay(); + } + result = "success"; + } catch (e) { + result = e; + } + } + + loop(); + + return result; + `.expectToEqual("success"); +}); + +test("can await already resolved promise", () => { + util.testFunction` + const result = []; + async function abc() { + return await Promise.resolve(30); + } + abc().then(value => result.push(value)); + + return result; + `.expectToEqual([30]); +}); + +test("can await already rejected promise", () => { + util.testFunction` + const result = []; + async function abc() { + return await Promise.reject("test rejection"); + } + abc().catch(reason => result.push(reason)); + + return result; + `.expectToEqual(["test rejection"]); +}); + +test("can await pending promise", () => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(data => log("resolving original promise", data)); + + async function abc() { + return await promise; + } + + const awaitingPromise = abc(); + awaitingPromise.then(data => log("resolving awaiting promise", data)); + + resolve("resolved data"); + + return allLogs; + + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["resolving original promise", "resolved data", "resolving awaiting promise", "resolved data"]); +}); + +test("can await non-promise values", () => { + util.testFunction` + async function foo() { + return await "foo"; + } + + async function bar() { + return await { foo: await foo(), bar: "bar" }; + } + + async function baz() { + return (await bar()).foo + (await bar()).bar; + } + + const { state, value } = baz() as any; + return { state, value }; + `.expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "foobar", + }); +}); + +test.each(["async function abc() {", "const abc = async () => {"])( + "can return non-promise from async function (%p)", + functionHeader => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(data => log("resolving original promise", data)); + + ${functionHeader} + await promise; + return "abc return data" + } + + const awaitingPromise = abc(); + awaitingPromise.then(data => log("resolving awaiting promise", data)); + + resolve("resolved data"); + + return allLogs; + + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "resolving original promise", + "resolved data", + "resolving awaiting promise", + "abc return data", + ]); + } +); + +test.each(["async function abc() {", "const abc = async () => {"])( + "can have multiple awaits in async function (%p)", + functionHeader => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + promise1.then(data => log("resolving promise1", data)); + promise2.then(data => log("resolving promise2", data)); + promise3.then(data => log("resolving promise3", data)); + + ${functionHeader} + const result1 = await promise1; + const result2 = await promise2; + const result3 = await promise3; + return [result1, result2, result3]; + } + + const awaitingPromise = abc(); + awaitingPromise.then(data => log("resolving awaiting promise", data)); + + resolve1("data1"); + resolve2("data2"); + resolve3("data3"); + + return allLogs; + + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "resolving promise1", + "data1", + "resolving promise2", + "data2", + "resolving promise3", + "data3", + "resolving awaiting promise", + ["data1", "data2", "data3"], + ]); + } +); + +test("can make inline async functions", () => { + util.testFunction` + const foo = async function() { return "foo"; }; + const bar = async function() { return await foo(); }; + + const { state, value } = bar() as any; + return { state, value }; + `.expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "foo", + }); +}); + +test("can make async lambdas with expression body", () => { + util.testFunction` + const foo = async () => "foo"; + const bar = async () => await foo(); + + const { state, value } = bar() as any; + return { state, value }; + `.expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "foo", + }); +}); + +test("can await async function from async function", () => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(data => log("resolving original promise", data)); + + async function abc() { + return await promise; + } + + async function def() { + return await abc(); + } + + const awaitingPromise = def(); + awaitingPromise.then(data => log("resolving awaiting promise", data)); + + resolve("resolved data"); + + return allLogs; + + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["resolving original promise", "resolved data", "resolving awaiting promise", "resolved data"]); +}); + +test("async function returning value is same as non-async function returning promise", () => { + util.testFunction` + function f(): Promise { + return Promise.resolve(42); + } + + async function fAsync(): Promise { + return 42; + } + + const { state: state1, value: value1 } = f() as any; + const { state: state2, value: value2 } = fAsync() as any; + + return { + state1, value1, + state2, value2 + }; + `.expectToEqual({ + state1: 1, // __TS__PromiseState.Fulfilled + value1: 42, + state2: 1, // __TS__PromiseState.Fulfilled + value2: 42, + }); +}); + +test("correctly handles awaited functions rejecting", () => { + util.testFunction` + const { promise: promise1, reject } = defer(); + const { promise: promise2 } = defer(); + + promise1.then(data => log("resolving promise1", data), reason => log("rejecting promise1", reason)); + promise2.then(data => log("resolving promise2", data)); + + async function abc() { + const result1 = await promise1; + const result2 = await promise2; + return [result1, result2]; + } + + const awaitingPromise = abc(); + awaitingPromise.catch(reason => log("awaiting promise was rejected because:", reason)); + + reject("test reject"); + + return allLogs; + + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["rejecting promise1", "test reject", "awaiting promise was rejected because:", "test reject"]); +}); + +test("can call async function at top-level", () => { + util.testModule` + export let aStarted = false; + async function a() { + aStarted = true; + return 42; + } + + a(); // Call async function (but cannot await) + ` + .setOptions({ module: ModuleKind.ESNext, target: ScriptTarget.ES2017 }) + .expectToEqual({ + aStarted: true, + }); +}); + +test("async function throws error", () => { + util.testFunction` + async function a() { + throw "test throw"; + } + + const { state, rejectionReason } = a() as any; + return { state, rejectionReason }; + `.expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "test throw", + }); +}); + +test("async lambda throws error", () => { + util.testFunction` + const a = async () => { + throw "test throw"; + } + + const { state, rejectionReason } = a() as any; + return { state, rejectionReason }; + `.expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "test throw", + }); +}); + +test("async function throws object", () => { + util.testFunction` + async function a() { + throw new Error("test throw"); + } + + const { state, rejectionReason } = a() as any; + return { state, rejectionReason }; + `.expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: { + message: "test throw", + name: "Error", + stack: expect.stringContaining("stack traceback"), + }, + }); +}); + +test.each([ + "await a();", + "const b = await a();", + "export const b = await a();", + "declare function foo(n: number): number; foo(await a());", + "declare function foo(n: number): number; const b = foo(await a());", + "const b = [await a()];", + "const b = [4, await a()];", + "const b = true ? 4 : await a();", +])("cannot await at top-level (%p)", awaitUsage => { + util.testModule` + async function a() { + return 42; + } + + ${awaitUsage} + export {} // Required to make TS happy, cannot await without import/exports + ` + .setOptions({ module: ModuleKind.ESNext, target: ScriptTarget.ES2017 }) + .expectToHaveDiagnostics([awaitMustBeInAsyncFunction.code]); +}); + +test("async function can access varargs", () => { + util.testFunction` + const { promise, resolve } = defer(); + + async function a(...args: string[]) { + log(await promise); + log(args[1]); + } + + const awaitingPromise = a("A", "B", "C"); + resolve("resolved"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["resolved", "B"]); +}); + +test("async function can forward varargs", () => { + util.testFunction` + const { promise, resolve } = defer(); + + async function a(...args: string[]) { + log(await promise); + log(...args); + } + + const awaitingPromise = a("A", "B", "C"); + resolve("resolved"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["resolved", "A", "B", "C"]); +}); + +test("async function adopts pending promise", () => { + util.testFunction` + let resolve: (v: string) => void = () => {}; + async function receive(): Promise { + return new Promise(res => { + resolve = res; + }); + } + + receive().then(v => { + log(v); + }); + + resolve("delayed resolve"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["delayed resolve"]); +}); + +test("async function adopts resolved promise", () => { + util.testFunction` + async function receive(): Promise { + return new Promise(resolve => { + resolve("resolved!"); + }); + } + + receive().then(v => { + log(v); + }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["resolved!"]); +}); + +test("async function adopts rejected promise", () => { + util.testFunction` + async function receive(): Promise { + return new Promise((_, reject) => { + reject("rejected"); + }); + } + + receive().catch(err => { + log(err); + }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["rejected"]); +}); + +test("return in catch aborts async method", () => { + util.testFunction` + async function asyncFunc() { + try { + await Promise.reject("rejection"); + log("Should not be seen"); + } catch { + log("C"); + return "result"; + } + log("Should also not be seen"); + } + + asyncFunc().then(v => log("resolved", v)); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["C", "resolved", "result"]); +}); + +test.each(["async function abc() {", "const abc = async () => {"])( + "can throw error after await in async function (%p)", + functionHeader => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(data => log("resolving first promise", data)); + + ${functionHeader} + await promise; + log("run abc"); + throw "test throw"; + } + + const awaitingPromise = abc(); + awaitingPromise.catch(error => log("caught error", error)); + + resolve("resolved data"); + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["resolving first promise", "resolved data", "run abc", "caught error", "test throw"]); + } +); + +test.each(["async function abc() {", "const abc = async () => {"])( + "can throw object after await in async function (%p)", + functionHeader => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(data => log("resolving first promise", data)); + + ${functionHeader} + await promise; + log("run abc"); + throw new Error("test throw"); + } + + const awaitingPromise = abc(); + awaitingPromise.catch(error => log("caught error", error)); + + resolve("resolved data"); + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "resolving first promise", + "resolved data", + "run abc", + "caught error", + { + message: "test throw", + name: "Error", + stack: expect.stringContaining("stack traceback"), + }, + ]); + } +); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1105 +describe("try/catch in async function", () => { + util.testEachVersion( + "await inside try/catch returns inside async function", + () => util.testModule` + export let result = 0; + async function foo(): Promise { + try { + return await new Promise(resolve => resolve(4)); + } catch { + throw "an error occurred in the async function" + } + } + foo().then(value => { + result = value; + }); + `, + // Cannot execute LuaJIT with test runner + { + ...util.expectEachVersionExceptJit(builder => builder.expectToEqual({ result: 4 })), + [LuaTarget.Lua50]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + [LuaTarget.Lua51]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + } + ); + + util.testEachVersion( + "await inside try/catch throws inside async function", + () => util.testModule` + export let reason = ""; + async function foo(): Promise { + try { + return await new Promise((resolve, reject) => reject("test error")); + } catch (e) { + throw "an error occurred in the async function: " + e; + } + } + foo().catch(e => { + reason = e; + }); + `, + { + ...util.expectEachVersionExceptJit(builder => + builder.expectToEqual({ reason: "an error occurred in the async function: test error" }) + ), + [LuaTarget.Lua50]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + [LuaTarget.Lua51]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + } + ); + + util.testEachVersion( + "await inside try/catch deferred rejection uses catch clause", + () => + util.testModule` + export let reason = ""; + let reject: (reason: string) => void; + + async function foo(): Promise { + try { + return await new Promise((res, rej) => { reject = rej; }); + } catch (e) { + throw "an error occurred in the async function: " + e; + } + } + foo().catch(e => { + reason = e; + }); + reject("test error"); + `, + { + ...util.expectEachVersionExceptJit(builder => + builder.expectToEqual({ reason: "an error occurred in the async function: test error" }) + ), + [LuaTarget.Lua50]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + [LuaTarget.Lua51]: builder => + builder.expectToHaveDiagnostics([unsupportedForTargetButOverrideAvailable.code]), + } + ); + + test("try/finally in async function", () => { + util.testModule` + const foo = async () => { + throw "foo error"; + }; + + let finallyCalled = false; + + const run = async () => { + try { + await foo(); + } finally { + finallyCalled = true; + } + return 10; + }; + + let succeeded: any = false; + let rejected: any = false; + + run() + .then(_ => { succeeded = true }) + .catch(e => { rejected = e }); + + export const result = { finallyCalled, succeeded, rejected }; + `.expectToEqual({ + result: { + finallyCalled: true, + succeeded: false, + rejected: "foo error", + }, + }); + }); + + test("async function adopts pending promise in try", () => { + util.testFunction` + let resolve: (v: string) => void = () => {}; + async function receive(): Promise { + try + { + return new Promise(res => { + resolve = res; + }); + } + catch + { + } + } + + receive().then(v => { + log(v); + }); + + resolve("delayed resolve"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["delayed resolve"]); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1272 + test("Awaiting a rejected promise should halt function execution (#1272)", () => { + util.testModule` + const foo = async () => { + throw new Error('foo error'); + }; + + let halted = true; + let caught = ""; + + const run = async () => { + try { + await foo(); + halted = false; + } catch (err) { + caught = 'catch err: ' + err; + } + }; + + let succeeded: any = false; + let rejected: any = false; + + run() + .then(_ => { succeeded = true }, + e => { rejected = e }); + + export const result = { halted, caught, succeeded, rejected }; + `.expectToEqual({ + result: { + halted: true, + caught: "catch err: Error: foo error", + succeeded: true, + rejected: false, + }, + }); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1272 + test("Awaiting a rejecting promise should halt function execution (#1272)", () => { + util.testModule` + let rej: (error: any) => void = () => {}; + const foo = () => new Promise((_, reject) => { + rej = reject; + }); + + let halted = true; + let caught = ""; + + const run = async () => { + try { + await foo(); + halted = false; + } catch (err) { + caught = 'catch err: ' + err; + } + }; + + let succeeded: any = false; + let rejected: any = false; + + run() + .then(_ => { succeeded = true }, + e => { rejected = e }); + + rej("rejection message"); + + export const result = { halted, caught, succeeded, rejected }; + `.expectToEqual({ + result: { + halted: true, + caught: "catch err: rejection message", + succeeded: true, + rejected: false, + }, + }); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1272 + test("Async try/catch complicated case (#1272)", () => { + util.testModule` + const foo = async () => { + throw new Error('foo error'); + }; + + let halted = true; + let caught = ""; + + const run = async () => { + try { + await foo(); // This throws + } catch (err) { + caught = 'catch err: ' + err; // This catches the async thrown error, function continues + } + try { + throw "throw 2"; // Another throw + } catch (err) { + try { + await foo(); + } catch (err2) { + return \`caught: \${err}, then: \${err2}\`; // Async function should exit here + } + } + halted = false; // This code should not be called + return 10; + }; + + let succeeded: any = false; + let rejected: any = false; + let value: any = undefined; + + run() + .then(v => { succeeded = true; value = v }, + e => { rejected = e }); + + export const result = { halted, caught, succeeded, rejected, value }; + `.expectToEqual({ + result: { + halted: true, + caught: "catch err: Error: foo error", + succeeded: true, + rejected: false, + value: "caught: throw 2, then: Error: foo error", + }, + }); + }); +}); diff --git a/test/unit/builtins/console.spec.ts b/test/unit/builtins/console.spec.ts new file mode 100644 index 000000000..a68c1be5c --- /dev/null +++ b/test/unit/builtins/console.spec.ts @@ -0,0 +1,82 @@ +import * as util from "../../util"; + +const compilerOptions = { lib: ["lib.esnext.d.ts", "lib.dom.d.ts"] }; + +test.each([ + "console.log()", + 'console.log("Hello")', + 'console.log("Hello %s", "there")', + 'console.log("Hello %%s", "there")', + 'console.log("Hello", "There")', +])("console.log (%p)", code => { + util.testFunction(code).setOptions(compilerOptions).expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.info()", + 'console.info("Hello")', + 'console.info("Hello %s", "there")', + 'console.info("Hello %%s", "there")', + 'console.info("Hello", "There")', +])("console.info (%p)", code => { + util.testFunction(code).setOptions(compilerOptions).expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.error()", + 'console.error("Hello")', + 'console.error("Hello %s", "there")', + 'console.error("Hello %%s", "there")', + 'console.error("Hello", "There")', +])("console.error (%p)", code => { + util.testFunction(code).setOptions(compilerOptions).expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.warn()", + 'console.warn("Hello")', + 'console.warn("Hello %s", "there")', + 'console.warn("Hello %%s", "there")', + 'console.warn("Hello", "There")', +])("console.warn (%p)", code => { + util.testFunction(code).setOptions(compilerOptions).expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.trace()", + 'console.trace("message")', + 'console.trace("Hello %s", "there")', + 'console.trace("Hello %%s", "there")', + 'console.trace("Hello", "there")', +])("console.trace (%p)", code => { + util.testFunction(code).setOptions(compilerOptions).expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.assert(false)", + 'console.assert(false, "message")', + 'console.assert(false, "message %s", "info")', + 'console.assert(false, "message %%s", "info")', + 'console.assert(false, "message", "more")', +])("console.assert (%p)", code => { + util.testFunction(code).setOptions(compilerOptions).expectLuaToMatchSnapshot(); +}); + +test("console.differentiation", () => { + util.testModule` + export class Console { + public test() { + return 42; + } + } + + function test() { + const console = new Console(); + return console.test(); + } + + export const result = test(); + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/globalThis.spec.ts b/test/unit/builtins/globalThis.spec.ts new file mode 100644 index 000000000..85463f7ff --- /dev/null +++ b/test/unit/builtins/globalThis.spec.ts @@ -0,0 +1,32 @@ +import * as util from "../../util"; + +test("equals _G", () => { + util.testExpression`globalThis === _G`.setTsHeader("declare const _G: typeof globalThis;").expectToEqual(true); +}); + +test("registers global symbol", () => { + util.testFunction` + globalThis.foo = "bar"; + return foo; + ` + .setTsHeader("declare global { var foo: string }") + .expectToEqual("bar"); +}); + +test("uses global symbol", () => { + util.testFunction` + foo = "bar"; + return globalThis.foo; + ` + .setTsHeader("declare global { var foo: string }") + .expectToEqual("bar"); +}); + +test("function call", () => { + util.testFunction` + foo = () => "bar"; + return globalThis.foo(); + ` + .setTsHeader("declare global { var foo: () => string }") + .expectToEqual("bar"); +}); diff --git a/test/unit/builtins/loading.spec.ts b/test/unit/builtins/loading.spec.ts new file mode 100644 index 000000000..563870f6d --- /dev/null +++ b/test/unit/builtins/loading.spec.ts @@ -0,0 +1,87 @@ +import * as tstl from "../../../src"; +import { unsupportedProperty } from "../../../src/transformation/utils/diagnostics"; +import * as util from "../../util"; + +describe("luaLibImport", () => { + test("inline", () => { + util.testExpression`[0].indexOf(1)` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Inline }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain('require("lualib_bundle")')) + .expectToMatchJsResult(); + }); + + test("require", () => { + util.testExpression`[0].indexOf(1)` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain('require("lualib_bundle")')) + .expectToMatchJsResult(); + }); + + function testLualibOnlyHasArrayIndexOf(builder: util.TestBuilder) { + const lualibBundle = builder.getLuaResult().transpiledFiles.find(f => f.outPath.endsWith("lualib_bundle.lua"))!; + expect(lualibBundle).toBeDefined(); + expect(lualibBundle.lua).toEqual(expect.stringMatching("^local function __TS__ArrayIndexOf")); + expect(lualibBundle.lua).not.toContain("__TS__ArrayConcat"); + expect(lualibBundle.lua).not.toContain("Error"); + } + + test("require-minimal", () => { + util.testExpression`[0].indexOf(1)` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.RequireMinimal }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain('require("lualib_bundle")')) + .tap(testLualibOnlyHasArrayIndexOf) + .expectToMatchJsResult(); + }); + + test("require-minimal with lualib in another file", () => { + util.testModule` + import "./other"; + ` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.RequireMinimal }) + .addExtraFile( + "other.lua", + ` +local ____lualib = require("lualib_bundle") +local __TS__ArrayIndexOf = ____lualib.__TS__ArrayIndexOf +__TS__ArrayIndexOf({}, 1) + ` + ) + // note: indent matters in above code, because searching for lualib checks for start & end of line + .tap(testLualibOnlyHasArrayIndexOf) + .expectNoExecutionError(); + }); +}); + +test.each([ + tstl.LuaLibImportKind.Inline, + tstl.LuaLibImportKind.None, + tstl.LuaLibImportKind.Require, + tstl.LuaLibImportKind.RequireMinimal, +])("should not include lualib without code (%p)", luaLibImport => { + util.testModule``.setOptions({ luaLibImport }).tap(builder => expect(builder.getMainLuaCodeChunk()).toBe("")); +}); + +test("lualib should not include tstl header", () => { + util.testExpression`[0].indexOf(1)`.tap(builder => + expect(builder.getMainLuaCodeChunk()).not.toContain("Generated with") + ); +}); + +test("using lualib does not crash when coroutine is not defined", () => { + util.testModule` + declare const _G: any; + declare function require(this: void, name: string): any + + _G.coroutine = undefined; + require("lualib_bundle"); + export const result = 1 + `.expectToEqual({ result: 1 }); +}); + +describe("Unknown builtin property", () => { + test("access", () => { + util.testExpression`Math.unknownProperty` + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedProperty.code]); + }); +}); diff --git a/test/unit/builtins/map.spec.ts b/test/unit/builtins/map.spec.ts new file mode 100644 index 000000000..f2e1cbfbe --- /dev/null +++ b/test/unit/builtins/map.spec.ts @@ -0,0 +1,255 @@ +import * as util from "../../util"; + +test("map constructor", () => { + util.testFunction` + let mymap = new Map(); + return mymap.size; + `.expectToMatchJsResult(); +}); + +test("map iterable constructor", () => { + util.testFunction` + let mymap = new Map([["a", "c"],["b", "d"]]); + return mymap.has("a") && mymap.has("b"); + `.expectToMatchJsResult(); +}); + +test("map iterable constructor map", () => { + util.testFunction` + let mymap = new Map(new Map([["a", "c"],["b", "d"]])); + return mymap.has("a") && mymap.has("b"); + `.expectToMatchJsResult(); +}); + +test("map clear", () => { + const mapTS = 'let mymap = new Map([["a", "c"],["b", "d"]]); mymap.clear();'; + util.testExpression("mymap.size").setTsHeader(mapTS).expectToMatchJsResult(); + + util.testExpression('!mymap.has("a") && !mymap.has("b")').setTsHeader(mapTS).expectToMatchJsResult(); +}); + +test("map delete", () => { + util.testFunction` + let mymap = new Map([["a", "c"],["b", "d"]]); + mymap.delete("a"); + return mymap.has("b") && !mymap.has("a"); + `.expectToMatchJsResult(); +}); + +test("map entries", () => { + util.testFunction` + let mymap = new Map([[5, 2],[6, 3],[7, 4]]); + let count = 0; + for (const [key, value] of mymap.entries()) { count += key + value; } + return count; + `.expectToMatchJsResult(); +}); + +test("map foreach", () => { + util.testFunction( + `let mymap = new Map([["a", 2],["b", 3],["c", 4]]); + let count = 0; + mymap.forEach(i => count += i); + return count;` + ).expectToMatchJsResult(); +}); + +test("map foreach keys", () => { + util.testFunction` + let mymap = new Map([[5, 2],[6, 3],[7, 4]]); + let count = 0; + mymap.forEach((value, key) => { count += key; }); + return count; + `.expectToMatchJsResult(); +}); + +test("map get", () => { + util.testFunction` + let mymap = new Map([["a", "c"],["b", "d"]]); + return mymap.get("a"); + `.expectToMatchJsResult(); +}); + +test("map get missing", () => { + util.testFunction` + let mymap = new Map([["a", "c"],["b", "d"]]); + return mymap.get("c"); + `.expectToMatchJsResult(); +}); + +test("map has", () => { + util.testFunction` + let mymap = new Map([["a", "c"]]); + return mymap.has("a"); + `.expectToMatchJsResult(); +}); + +test("map has false", () => { + util.testFunction` + let mymap = new Map(); + return mymap.has("a"); + `.expectToMatchJsResult(); +}); + +test.each([ + '[["a", null]]', + '[["b", "c"], ["a", null]]', + '[["a", null], ["b", "c"]]', + '[["b", "c"], ["a", null], ["x", "y"]]', +])("map (%p) has null", entries => { + util.testFunction` + let mymap = new Map(${entries}); + return mymap.has("a"); + `.expectToMatchJsResult(); +}); + +test.each([ + '[["a", undefined]]', + '[["b", "c"], ["a", undefined]]', + '[["a", undefined], ["b", "c"]]', + '[["b", "c"], ["a", undefined], ["x", "y"]]', +])("map (%p) has undefined", entries => { + util.testFunction` + let mymap = new Map(${entries}); + return mymap.has("a"); + `.expectToMatchJsResult(); +}); + +test("map keys", () => { + util.testFunction` + let mymap = new Map([[5, 2],[6, 3],[7, 4]]); + let count = 0; + for (const key of mymap.keys()) { count += key; } + return count; + `.expectToMatchJsResult(); +}); + +test("map set", () => { + const mapTS = 'let mymap = new Map(); mymap.set("a", 5);'; + util.testFunction(mapTS + 'return mymap.has("a");').expectToMatchJsResult(); + + util.testFunction(mapTS + 'return mymap.get("a")').expectToMatchJsResult(); +}); + +test("map values", () => { + util.testFunction` + let mymap = new Map([[5, 2],[6, 3],[7, 4]]); + let count = 0; + for (const value of mymap.values()) { count += value; } + return count; + `.expectToMatchJsResult(); +}); + +test("map size", () => { + util.testFunction("let m = new Map(); return m.size;").expectToMatchJsResult(); + util.testFunction("let m = new Map(); m.set(1,3); return m.size;").expectToMatchJsResult(); + util.testFunction("let m = new Map([[1,2],[3,4]]); return m.size;").expectToMatchJsResult(); + util.testFunction("let m = new Map([[1,2],[3,4]]); m.clear(); return m.size;").expectToMatchJsResult(); + util.testFunction("let m = new Map([[1,2],[3,4]]); m.delete(3); return m.size;").expectToMatchJsResult(); +}); + +const iterationMethods = ["entries", "keys", "values"]; +describe.each(iterationMethods)("map.%s() preserves insertion order", iterationMethod => { + test("basic", () => { + util.testFunction` + const mymap = new Map(); + + mymap.set("x", 1); + mymap.set("a", 2); + mymap.set(4, 3); + mymap.set("b", 6); + mymap.set(1, 4); + mymap.set("a", 5); + + mymap.delete("b"); + + return [...mymap.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); + + test("after removing last", () => { + util.testFunction` + const mymap = new Map(); + + mymap.set("x", 1); + mymap.set("a", 2); + mymap.set(4, 3); + + mymap.delete(4); + + return [...mymap.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); + + test("after removing first", () => { + util.testFunction` + const mymap = new Map(); + + mymap.set("x", 1); + mymap.set("a", 2); + mymap.set(4, 3); + + mymap.delete("x"); + + return [...mymap.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); + + test("after removing all", () => { + util.testFunction` + const mymap = new Map(); + + mymap.set("x", 1); + mymap.set("a", 2); + + mymap.delete("a"); + mymap.delete("x"); + + return [...mymap.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); +}); + +describe("Map.groupBy", () => { + test("empty", () => { + util.testFunction` + const array = []; + + const map = Map.groupBy(array, (num, index) => { + return num % 2 === 0 ? "even": "odd"; + }); + + return Object.fromEntries(map.entries()); + `.expectToEqual([]); + }); + + test("groupBy", () => { + util.testFunction` + const array = [0, 1, 2, 3, 4, 5]; + + const map = Map.groupBy(array, (num, index) => { + return num % 2 === 0 ? "even": "odd"; + }); + + return Object.fromEntries(map.entries()); + `.expectToEqual({ + even: [0, 2, 4], + odd: [1, 3, 5], + }); + }); + + test("groupBy index", () => { + util.testFunction` + const array = [0, 1, 2, 3, 4, 5]; + + const map = Map.groupBy(array, (num, index) => { + return index < 3 ? "low": "high"; + }); + + return Object.fromEntries(map.entries()); + `.expectToEqual({ + low: [0, 1, 2], + high: [3, 4, 5], + }); + }); +}); diff --git a/test/unit/builtins/math.spec.ts b/test/unit/builtins/math.spec.ts new file mode 100644 index 000000000..f3ff1f306 --- /dev/null +++ b/test/unit/builtins/math.spec.ts @@ -0,0 +1,113 @@ +import * as tstl from "../../../src"; +import * as util from "../../util"; + +// These test are kept to a minimum, +// because we just want to confirm translation is correct +// and not test Lua's built in math functions. +// Differences in math implementations between JS & Lua cause inaccuracies +// therefore test input numbers are "carefully" selected to always match accuratly. +// Lualib implementations are tested separately. +test.each([ + // log + "Math.log(42)", + "Math.log10(10)", + "Math.log2(42)", + "Math.log1p(42)", + // round + "Math.round(0.1)", + "Math.round(0.9)", + "Math.round(0.5)", + // abs + "Math.abs(-42)", + "Math.abs(42)", + // trigometric + "Math.acos(0.42)", + "Math.asin(0.42)", + "Math.atan(0.42)", + "Math.cos(42)", + "Math.sin(42)", + "Math.tan(42)", + "Math.PI", + // ceil & floor + "Math.ceil(42.42)", + "Math.floor(42.42)", + // exp + "Math.exp(42)", + // max & min + "Math.max(-42, 42)", + "Math.max(42, -42)", + "Math.max(42, 42)", + "Math.max(-42, -42)", + "Math.min(42, -42)", + "Math.min(-42, 42)", + "Math.min(42, 42)", + "Math.min(-42, -42)", + // pow + "Math.pow(4.2, 4.2)", + "Math.pow(4.2, -4.2)", + "Math.pow(-4.2, -4.2)", + // random + // "Math.random()", + // sqrt + "Math.sqrt(2)", + "Math.sqrt(-2)", + // trunc + "Math.trunc(42.42)", + "Math.trunc(-42.42)", + "Math.trunc(0)", + "Math.trunc(Infinity)", + "Math.trunc(-Infinity)", +])("%s", code => { + util.testExpression(code).expectToMatchJsResult(); +}); + +// Hard to test properly +util.testExpression("Math.random()").expectNoExecutionError(); + +test.each(["E", "LN10", "LN2", "LOG10E", "LOG2E", "SQRT1_2", "SQRT2"])("Math.%s", constant => { + util.testExpression`Math.${constant}`.tap(builder => { + expect(builder.getLuaExecutionResult()).toBeCloseTo(builder.getJsExecutionResult()); + }); +}); + +// LuaLib MathSign +test.each([ + "Math.sign(-42)", + "Math.sign(42)", + "Math.sign(-4.2)", + "Math.sign(4.2)", + "Math.sign(0)", + "Math.sign(-Infinity)", +])("%s", code => { + util.testExpression(code).expectToMatchJsResult(); +}); + +// LuaLib Atan2 +const expectMathAtan2: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("math.atan2("); +const expectMathAtan: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("math.atan("); +const expectLualibMathAtan2: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).toContain("__TS__MathAtan2("); + +util.testEachVersion("Math.atan2", () => util.testExpression`Math.atan2(4, 5)`, { + [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibMathAtan2), + [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Lua50]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Lua52]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Lua53]: builder => builder.tap(expectMathAtan), + [tstl.LuaTarget.Lua54]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Lua55]: builder => builder.tap(expectMathAtan2), + [tstl.LuaTarget.Luau]: builder => builder.tap(expectMathAtan2), +}); + +util.testEachVersion( + "Math.atan2(4, 5)", + () => util.testExpression`Math.atan2(4, 5)`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); + +util.testEachVersion( + "Math.pow(3, 5)", + () => util.testExpression`Math.pow(3, 5)`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts new file mode 100644 index 000000000..aa3a8927d --- /dev/null +++ b/test/unit/builtins/numbers.spec.ts @@ -0,0 +1,241 @@ +import * as util from "../../util"; + +test.each([ + "NaN + NaN", + "NaN - NaN", + "NaN * NaN", + "NaN / NaN", + "NaN + 1", + "1 + NaN", + "1 / NaN", + "NaN * 0", + + "Infinity", + "Infinity - Infinity", + "Infinity / -1", + "Infinity * -1", + "Infinity + 1", + "Infinity - 1", +])("%s", code => { + util.testExpression(code).expectToMatchJsResult(); +}); + +// Current implementation does not allow this +// eslint-disable-next-line jest/no-disabled-tests +test.skip.each(["NaN", "Infinity"])("%s reassignment", name => { + util.testFunction` + const ${name} = 1; + return ${name}; + `.expectToMatchJsResult(); +}); + +const numberCases = [-1, 0, 1, 1.5, Infinity, -Infinity]; +const stringCases = ["-1", "0", "1", "1.5", "Infinity", "-Infinity"]; +const restCases = [true, false, "", " ", "\t", "\n", "foo", {}]; +const cases = [...numberCases, ...stringCases, ...restCases]; + +describe("Number", () => { + test.each(cases)("constructor(%p)", value => { + util.testExpressionTemplate`Number(${value})`.expectToMatchJsResult(); + }); + + test.each(cases)("isNaN(%p)", value => { + util.testExpressionTemplate`Number.isNaN(${value} as any)`.expectToMatchJsResult(); + }); + + test.each(cases)("isFinite(%p)", value => { + util.testExpressionTemplate`Number.isFinite(${value} as any)`.expectToMatchJsResult(); + }); + + test.each(cases)("isInteger(%p)", value => { + util.testExpressionTemplate`Number.isInteger(${value} as any)`.expectToMatchJsResult(); + }); +}); + +const toStringRadixes = [undefined, 10, 2, 8, 9, 16, 17, 36, 36.9]; +const toStringValues = [-1, 0, 1, 1.5, 1024, 1.2]; +const toStringPairs = toStringValues.flatMap(value => toStringRadixes.map(radix => [value, radix] as const)); + +test.each(toStringPairs)("(%p).toString(%p)", (value, radix) => { + util.testExpressionTemplate`(${value}).toString(${radix})`.expectToMatchJsResult(); +}); + +test.each([ + [NaN, "(0/0)"], + [Infinity, "(1/0)"], + [-Infinity, "(-(1/0))"], +])("%p.toString(2)", (value, luaNativeSpecialNum) => { + // Need to get the actual lua tostring version of inf/nan + // this is platform dependent so we can/should not hardcode it + const luaNativeSpecialNumString = util.testExpression`${luaNativeSpecialNum}.toString()`.getLuaExecutionResult(); + // Cannot use expectToMatchJsResult because this actually wont be the same in JS in Lua + // TODO fix this in lualib/NumberToString.ts + util.testExpressionTemplate`(${value}).toString(2)`.expectToEqual(luaNativeSpecialNumString); +}); + +const toFixedFractions = [undefined, 0, 1, 2, Math.PI, 5, 99]; +// 1.5, 1.25 and 1.125 fails as rounding differ +const toFixedValues = [-1, 0, 1, Math.PI, -1.1234, -9.99e19, 1e22]; +const toFixedPairs = toFixedValues.flatMap(value => toFixedFractions.map(frac => [value, frac] as const)); +test.each(toFixedPairs)("(%p).toFixed(%p)", (value, frac) => { + util.testExpressionTemplate`(${value}).toFixed(${frac})`.expectToMatchJsResult(); +}); + +test.each([ + [NaN, "(0/0)"], + [Infinity, "(1/0)"], + [-Infinity, "(-(1/0))"], +])("%p.toFixed(2)", (value, luaNativeSpecialNum) => { + // Need to get the actual lua tostring version of inf/nan + // this is platform dependent so we can/should not hardcode it + const luaNativeSpecialNumString = util.testExpression`${luaNativeSpecialNum}.toString()`.getLuaExecutionResult(); + // Cannot use expectToMatchJsResult because this actually wont be the same in JS in Lua + util.testExpressionTemplate`(${value}).toFixed(2)`.expectToEqual(luaNativeSpecialNumString); +}); + +test.each(cases)("isNaN(%p)", value => { + util.testExpressionTemplate`isNaN(${value} as any)`.expectToMatchJsResult(); +}); + +test.each(cases)("isFinite(%p)", value => { + util.testExpressionTemplate`isFinite(${value} as any)`.expectToMatchJsResult(); +}); + +test("number intersected method", () => { + util.testFunction` + type Vector = number & { normalize(): Vector }; + return ({ normalize: () => 3 } as Vector).normalize(); + `.expectToMatchJsResult(); +}); + +test("numbers overflowing the float limit become math.huge", () => { + util.testExpression`1e309`.expectToMatchJsResult(); +}); + +describe.each(["parseInt", "parseFloat", "Number.parseInt", "Number.parseFloat"])( + "parse numbers with %s", + parseFunction => { + const numberStrings = ["3", "3.0", "9", "42", "239810241", "-20391", "3.1415", "2.7182", "-34910.3"]; + + test.each(numberStrings)("parses (%s)", numberString => { + util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); + }); + + test("empty string", () => { + util.testExpression`${parseFunction}("")`.expectToMatchJsResult(); + }); + + test("invalid string", () => { + util.testExpression`${parseFunction}("bla")`.expectToMatchJsResult(); + }); + + test.each(["1px", "2300m", "3,4", "452adkfl"])("trailing text (%s)", numberString => { + util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); + }); + + test.each([" 3", " 4", " -231", " 1px"])("leading whitespace (%s)", numberString => { + util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); + }); + } +); + +test.each(["Infinity", "-Infinity", " -Infinity"])("parseFloat handles Infinity", numberString => { + util.testExpression`parseFloat("${numberString}")`.expectToMatchJsResult(); +}); + +test.each([ + { numberString: "36", base: 8 }, + { numberString: "-36", base: 8 }, + { numberString: "100010101101", base: 2 }, + { numberString: "-100010101101", base: 2 }, + { numberString: "3F", base: 16 }, +])("parseInt with base (%p)", ({ numberString, base }) => { + util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +}); + +test.each(["0x4A", "-0x42", "0X42", " 0x391", " -0x8F"])("parseInt detects hexadecimal", numberString => { + util.testExpression`parseInt("${numberString}")`.expectToMatchJsResult(); +}); + +test.each([1, 37, -100])("parseInt with invalid base (%p)", base => { + util.testExpression`parseInt("11111", ${base})`.expectToMatchJsResult(); +}); + +test.each([ + { numberString: "36px", base: 8 }, + { numberString: "10001010110231", base: 2 }, + { numberString: "3Fcolor", base: 16 }, +])("parseInt with base and trailing text (%p)", ({ numberString, base }) => { + util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +}); + +test.each(["Infinity", "-Infinity", " -Infinity"])("Number.parseFloat handles Infinity", numberString => { + util.testExpression`Number.parseFloat("${numberString}")`.expectToMatchJsResult(); +}); + +test.each([ + { numberString: "36", base: 8 }, + { numberString: "-36", base: 8 }, + { numberString: "100010101101", base: 2 }, + { numberString: "-100010101101", base: 2 }, + { numberString: "3F", base: 16 }, +])("Number.parseInt with base (%p)", ({ numberString, base }) => { + util.testExpression`Number.parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +}); + +test.each(["0x4A", "-0x42", "0X42", " 0x391", " -0x8F"])("Number.parseInt detects hexadecimal", numberString => { + util.testExpression`Number.parseInt("${numberString}")`.expectToMatchJsResult(); +}); + +test.each([1, 37, -100])("Number.parseInt with invalid base (%p)", base => { + util.testExpression`Number.parseInt("11111", ${base})`.expectToMatchJsResult(); +}); + +test.each([ + { numberString: "36px", base: 8 }, + { numberString: "10001010110231", base: 2 }, + { numberString: "3Fcolor", base: 16 }, +])("Number.parseInt with base and trailing text (%p)", ({ numberString, base }) => { + util.testExpression`Number.parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +}); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["42", "undefined"])("prototype call on nullable number (%p)", value => { + util.testFunction` + function toString(n?: number) { + return n?.toString(); + } + return toString(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); + +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", + "Number.MIN_SAFE_INTEGER < 0", + + "0 < Number.EPSILON", + "Number.EPSILON < Number.MAX_SAFE_INTEGER", +])("Numer constants have correct relative sizes (%p)", comparison => { + util.testExpression(comparison).expectToEqual(true); +}); + +// 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 new file mode 100644 index 000000000..1edc8a9d6 --- /dev/null +++ b/test/unit/builtins/object.spec.ts @@ -0,0 +1,304 @@ +import * as util from "../../util"; + +test.each([ + { initial: { a: 3 }, args: [{}] }, + { initial: {}, args: [{ a: 3 }] }, + { initial: { a: 3 }, args: [{ a: 5 }] }, + { initial: { a: 3 }, args: [{ b: 5 }, { c: 7 }] }, +])("Object.assign (%p)", ({ initial, args }) => { + const argsString = util.formatCode(...args); + util.testExpression`Object.assign(${util.formatCode(initial)}, ${argsString})`.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 + expect(testBuilder.getJsExecutionResult()).toEqual(expect.arrayContaining(testBuilder.getLuaExecutionResult())); +}); + +test.each([{}, { abc: 3 }, { abc: 3, def: "xyz" }])("Object.keys (%p)", obj => { + const testBuilder = util.testExpressionTemplate`Object.keys(${obj})`; + // Need custom matcher because order is not guaranteed in neither JS nor Lua + expect(testBuilder.getJsExecutionResult()).toEqual(expect.arrayContaining(testBuilder.getLuaExecutionResult())); +}); + +test.each([{}, { abc: "def" }, { abc: 3, def: "xyz" }])("Object.values (%p)", obj => { + const testBuilder = util.testExpressionTemplate`Object.values(${obj})`; + // Need custom matcher because order is not guaranteed in neither JS nor Lua + expect(testBuilder.getJsExecutionResult()).toEqual(expect.arrayContaining(testBuilder.getLuaExecutionResult())); +}); + +test.each(["[]", '[["a", 1], ["b", 2]]', '[["a", 1], ["a", 2]]', 'new Map([["foo", "bar"]])'])( + "Object.fromEntries(%s)", + entries => { + util.testExpression`Object.fromEntries(${entries})`.expectToMatchJsResult(); + } +); + +describe(".toString()", () => { + const toStringDeclaration = ` + function toString(value: object) { + const result = value.toString(); + return result === "[object Object]" || result.startsWith("table: ") ? "table" : result; + } + `; + + test("class override", () => { + util.testFunction` + ${toStringDeclaration} + class A { + public toString() { + return "A"; + } + } + + return toString(new A()); + `.expectToMatchJsResult(); + }); + + test("inherited class override", () => { + util.testFunction` + ${toStringDeclaration} + class A { + public toString() { + return "A"; + } + } + + class B extends A {} + + return { A: toString(new A()), B: toString(new B()) }; + `.expectToMatchJsResult(); + }); + + test("don't affect inherited class", () => { + util.testFunction` + ${toStringDeclaration} + class A {} + + class B extends A { + public toString() { + return "B"; + } + } + + return { A: toString(new A()), B: toString(new B()) }; + `.expectToMatchJsResult(); + }); + + test("override inherited class override", () => { + util.testFunction` + ${toStringDeclaration} + class A { + public toString() { + return "A"; + } + } + + class B extends A { + public toString() { + return "B"; + } + } + + return { A: toString(new A()), B: toString(new B()) }; + `.expectToMatchJsResult(); + }); +}); + +describe(".hasOwnProperty()", () => { + test("class field", () => { + util.testFunction` + class A { + public field = true; + } + + return new A().hasOwnProperty("field"); + `.expectToMatchJsResult(); + }); + + test("class method", () => { + util.testFunction` + class A { + public method() {} + } + + return new A().hasOwnProperty("method"); + `.expectToMatchJsResult(); + }); +}); + +const trueFalseTests = [[true], [false]] as const; + +describe("Object.defineProperty", () => { + test.each(trueFalseTests)("writable (%p)", value => { + util.testFunction` + const foo = { bar: true }; + Object.defineProperty(foo, "bar", { writable: ${value} }); + let error = false; + try { + foo.bar = false; + } catch { + error = true; + } + return { foobar: foo.bar, error }; + `.expectToMatchJsResult(); + }); + + test.each(trueFalseTests)("configurable (%p)", value => { + util.testFunction` + const foo = { bar: true }; + Object.defineProperty(foo, "bar", { configurable: ${value} }); + try { delete foo.bar } catch {}; + return foo.bar; + `.expectToMatchJsResult(); + }); + + test("defines a new property", () => { + util.testFunction` + const foo: any = {}; + Object.defineProperty(foo, "bar", { value: true }); + return foo.bar; + `.expectToMatchJsResult(); + }); + + test("overwrites an existing property", () => { + util.testFunction` + const foo = { bar: false }; + Object.defineProperty(foo, "bar", { value: true }); + return foo.bar; + `.expectToMatchJsResult(); + }); + + test("default descriptor", () => { + util.testFunction` + const foo = {}; + Object.defineProperty(foo, "bar", {}); + return Object.getOwnPropertyDescriptor(foo, "bar"); + `.expectToMatchJsResult(); + }); + + test.each([ + ["{ value: true, get: () => true }"], + ["{ value: true, set: value => {} }"], + ["{ writable: true, get: () => true }"], + ])("invalid descriptor (%p)", props => { + util.testFunction` + const foo: any = {}; + let err = false; + + try { + Object.defineProperty(foo, "bar", ${props}); + } catch { + err = true; + } + + return { prop: foo.bar, err }; + `.expectToMatchJsResult(); + }); +}); + +describe("Object.getOwnPropertyDescriptor", () => { + test("descriptor is exactly the same as the last one set", () => { + util.testFunction` + const foo = {}; + Object.defineProperty(foo, "bar", {}); + return Object.getOwnPropertyDescriptor(foo, "bar"); + `.expectToMatchJsResult(); + }); +}); + +describe("Object.getOwnPropertyDescriptors", () => { + test("all descriptors match", () => { + util.testFunction` + const foo = { bar: true }; + Object.defineProperty(foo, "bar", {}); + return Object.getOwnPropertyDescriptors(foo); + `.expectToMatchJsResult(); + }); +}); + +describe("delete from object", () => { + test("delete from object", () => { + util.testFunction` + const obj = { foo: "bar", bar: "baz" }; + return [delete obj["foo"], obj]; + `.expectToMatchJsResult(); + }); + + test("delete nonexistent property", () => { + util.testFunction` + const obj = {} as any + return [delete obj["foo"], obj]; + `.expectToMatchJsResult(); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/993 + test("delete from object with metatable", () => { + util.testFunction` + const obj = { foo: "bar", bar: "baz" }; + setmetatable(obj, {}); + return [delete obj["foo"], obj]; + ` + .setTsHeader("declare function setmetatable(this: void, table: T, metatable: any): T;") + .expectToEqual([true, { bar: "baz" }]); + }); + + test("delete nonconfigurable own property", () => { + util.testFunction` + const obj = {} as any + Object.defineProperty(obj, "foo", { + configurable: false, + value: true + }) + try { + delete obj["foo"] + return "deleted" + } catch (e) { + return e instanceof TypeError + } + ` + .setOptions({ + strict: true, + }) + .expectToMatchJsResult(); + }); +}); + +describe("Object.groupBy", () => { + test("empty", () => { + util.testFunction` + const array = []; + + return Object.groupBy(array, (num, index) => { + return num % 2 === 0 ? "even": "odd"; + }); + `.expectToEqual([]); + }); + + test("groupBy", () => { + util.testFunction` + const array = [0, 1, 2, 3, 4, 5]; + + return Object.groupBy(array, (num, index) => { + return num % 2 === 0 ? "even": "odd"; + }); + `.expectToEqual({ + even: [0, 2, 4], + odd: [1, 3, 5], + }); + }); + + test("groupBy index", () => { + util.testFunction` + const array = [0, 1, 2, 3, 4, 5]; + + return Object.groupBy(array, (num, index) => { + return index < 3 ? "low": "high"; + }); + `.expectToEqual({ + low: [0, 1, 2], + high: [3, 4, 5], + }); + }); +}); diff --git a/test/unit/builtins/promise.spec.ts b/test/unit/builtins/promise.spec.ts new file mode 100644 index 000000000..cb18db994 --- /dev/null +++ b/test/unit/builtins/promise.spec.ts @@ -0,0 +1,1325 @@ +import * as util from "../../util"; + +const promiseTestLib = ` +// Some logging utility, useful for asserting orders of operations + +const allLogs: any[] = []; +function log(...values: any[]) { + allLogs.push(...values); +} + +// Create a promise and store its resolve and reject functions, useful for creating pending promises + +function defer() { + let resolve: (data: any) => void = () => {}; + let reject: (reason: string) => void = () => {}; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}`; + +test("can create resolved promise", () => { + util.testFunction` + const { state, value } = Promise.resolve(42) as any; + return { state, value }; + `.expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: 42, + }); +}); + +test("can create rejected promise", () => { + util.testFunction` + const { state, rejectionReason } = Promise.reject("test rejection") as any; + return { state, rejectionReason }; + `.expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "test rejection", + }); +}); + +test("promise constructor executor throwing rejects promise", () => { + util.testFunction` + const { state, rejectionReason } = new Promise(() => { throw "executor exception"; }) as any; + return { state, rejectionReason }; + `.expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "executor exception", + }); +}); + +test("promise can be resolved", () => { + util.testFunction` + const { promise, resolve } = defer(); + + let result: string | undefined; + let rejectResult: string | undefined; + + promise.then( + data => { result = data; }, + reason => { rejectResult = reason; } + ); + + const beforeResolve = result; + + resolve("Hello!"); + + const afterResolve = result; + + return { beforeResolve, afterResolve, rejectResult }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + beforeResolve: undefined, + afterResolve: "Hello!", + rejectResult: undefined, + }); +}); + +test("promise can be rejected", () => { + util.testFunction` + const { promise, reject } = defer(); + + let resolveResult: string | undefined; + let rejectResult: string | undefined; + + promise.then( + data => { resolveResult = data; }, + reason => { rejectResult = reason; } + ); + + const beforeReject = rejectResult; + + reject("Hello!"); + + const afterReject = rejectResult; + + return { beforeReject, afterReject, resolveResult }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + beforeReject: undefined, + afterReject: "Hello!", + resolveResult: undefined, + }); +}); + +test("promise cannot be resolved more than once", () => { + util.testFunction` + const { promise, resolve } = defer(); + + promise.then( + data => { log(data); } + ); + + resolve("Hello!"); + resolve("World!"); // Second resolve should be ignored + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["Hello!"]); +}); + +test("promise cannot be rejected more than once", () => { + util.testFunction` + const { promise, reject } = defer(); + + promise.then( + _ => {}, + reason => { log(reason); } + ); + + reject("Hello!"); + reject("World!"); // Second reject should be ignored + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["Hello!"]); +}); + +test("promise cannot be rejected after resolving", () => { + util.testFunction` + const { promise, resolve, reject } = defer(); + + promise.then( + data => { log(data); }, + reason => { log(reason); } + ); + + resolve("Hello!"); + reject("World!"); // should be ignored because already resolved + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["Hello!"]); +}); + +test("promise cannot be resolved after rejecting", () => { + util.testFunction` + const { promise, resolve, reject } = defer(); + + promise.then( + data => { log(data); }, + reason => { log(reason); } + ); + + reject("Hello!"); + resolve("World!"); // should be ignored because already rejected + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["Hello!"]); +}); + +test("promise can be (then-resolve) observed more than once", () => { + util.testFunction` + const { promise, resolve } = defer(); + + promise.then( + data => { log("then 1: " + data); } + ); + + promise.then( + data => { log("then 2: " + data); } + ); + + resolve("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["then 1: Hello!", "then 2: Hello!"]); +}); + +test("promise can be (then-reject) observed more than once", () => { + util.testFunction` + const { promise, reject } = defer(); + + promise.then( + undefined, + reason => { log("then 1: " + reason); } + ); + + promise.then( + undefined, + reason => { log("then 2: " + reason); }, + ); + + reject("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["then 1: Hello!", "then 2: Hello!"]); +}); + +test("promise can be (catch) observed more than once", () => { + util.testFunction` + const { promise, reject } = defer(); + + promise.catch( + reason => { log("catch 1: " + reason); } + ); + + promise.catch( + reason => { log("catch 2: " + reason); }, + ); + + reject("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["catch 1: Hello!", "catch 2: Hello!"]); +}); + +test("promise chained resolve resolves all", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + promise3.then(data => { + log("promise3: " + data); + resolve2(data); + }); + promise2.then(data => { + log("promise2: " + data); + resolve1(data); + }); + promise1.then(data => { + log("promise1: " + data); + }); + + resolve3("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise3: Hello!", "promise2: Hello!", "promise1: Hello!"]); +}); + +test("promise then returns a literal", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const promise2 = promise.then(data => { + log("promise resolved with: " + data); + return "promise 1 resolved: " + data; + }); + + promise2.then(data => { + log("promise2 resolved with: " + data); + }); + + resolve("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise resolved with: Hello!", "promise2 resolved with: promise 1 resolved: Hello!"]); +}); + +test("promise then returns a resolved promise", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const promise2 = promise.then(data => { + log("promise resolved with: " + data); + return Promise.resolve("promise 1 resolved: " + data); + }); + + promise2.then(data => { + log("promise2 resolved with: " + data); + }); + + resolve("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise resolved with: Hello!", "promise2 resolved with: promise 1 resolved: Hello!"]); +}); + +test("promise then returns a rejected promise", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const promise2 = promise.then(data => { + log("promise resolved with: " + data); + return Promise.reject("promise 1: reject!"); + }); + + promise2.catch(reason => { + log("promise2 rejected with: " + reason); + }); + + resolve("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise resolved with: Hello!", "promise2 rejected with: promise 1: reject!"]); +}); + +test("promise then returns a pending promise (resolves)", () => { + util.testFunction` + const { promise, resolve } = defer(); + + let resolve2: any; + + const promise2 = promise.then(data => { + log("promise resolved with: " + data); + + const promise3 = new Promise((res) => { + resolve2 = res; + }); + + promise3.then(data2 => { + log("promise3 resolved with: " + data2); + }); + + return promise3; + }); + + promise2.then(data => { + log("promise2 resolved with: " + data); + }); + + // Resolve promise 1 + resolve("Hello!"); + + // Resolve promise 2 and 3 + resolve2("World!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "promise resolved with: Hello!", + "promise3 resolved with: World!", + "promise2 resolved with: World!", + ]); +}); + +test("promise then returns a pending promise (rejects)", () => { + util.testFunction` + const { promise, resolve } = defer(); + + let reject: any; + + const promise2 = promise.then(data => { + log("promise resolved with: " + data); + + const promise3 = new Promise((_, rej) => { + reject = rej; + }); + + promise3.catch(reason => { + log("promise3 rejected with: " + reason); + }); + + return promise3; + }); + + promise2.catch(reason => { + log("promise2 rejected with: " + reason); + }); + + // Resolve promise 1 + resolve("Hello!"); + + // Reject promise 2 and 3 + reject("World!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "promise resolved with: Hello!", + "promise3 rejected with: World!", + "promise2 rejected with: World!", + ]); +}); + +test("promise then onFulfilled throws", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const promise2 = promise.then(data => { + throw "fulfill exception!" + }); + + promise2.catch(reason => { + log("promise2 rejected with: " + reason); + }); + + resolve("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise2 rejected with: fulfill exception!"]); +}); + +test("promise then onRejected throws", () => { + util.testFunction` + const { promise, reject } = defer(); + + const promise2 = promise.then( + _ => {}, + reason => { throw "fulfill exception from onReject!" } + ); + + promise2.catch(reason => { + log("promise2 rejected with: " + reason); + }); + + reject("Hello!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise2 rejected with: fulfill exception from onReject!"]); +}); + +test("then on resolved promise immediately calls callback", () => { + util.testFunction` + Promise.resolve(42).then(data => { log(data); }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([42]); +}); + +test("then on rejected promise immediately calls callback", () => { + util.testFunction` + Promise.reject("already rejected").then(data => { log("resolved", data); }, reason => { log("rejected", reason); }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["rejected", "already rejected"]); +}); + +test("second then throws", () => { + util.testFunction` + const { promise, resolve } = defer(); + + promise.then(data => { + // nothing + log("then1", data) + }); + + promise.then(data => { + log("then2", data) + throw "test throw"; + }).catch(err => { + // caught + log("rejected: ", err) + }); + + resolve("mydata"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["then1", "mydata", "then2", "mydata", "rejected: ", "test throw"]); +}); + +test("chained then throws", () => { + util.testFunction` + const { promise, resolve } = defer(); + + promise.then(data => { + // nothing + log("then1", data) + }).then(data => { + log("then2", data) + throw "test throw"; + }).catch(err => { + // caught + log("rejected: ", err) + }); + + resolve("mydata"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "then1", + "mydata", + "then2", // Does not have data because first then returned undefined + "rejected: ", + "test throw", + ]); +}); + +test("empty then resolves", () => { + util.testFunction` + const { promise, resolve } = defer(); + + promise.then().then(v => { log("then2", v) }); + + resolve("mydata"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["then2", "mydata"]); +}); + +test("empty then rejects", () => { + util.testFunction` + const { promise, reject } = defer(); + + promise.then().catch(err => { log("catch", err) }); + + reject("my error"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["catch", "my error"]); +}); + +test("catch on rejected promise immediately calls callback", () => { + util.testFunction` + Promise.reject("already rejected").catch(reason => { log(reason); }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["already rejected"]); +}); + +test("finally on resolved promise immediately calls callback", () => { + util.testFunction` + Promise.resolve(42).finally(() => { log("finally"); }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["finally"]); +}); + +test("finally on rejected promise immediately calls callback", () => { + util.testFunction` + Promise.reject("already rejected").finally(() => { log("finally"); }); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["finally"]); +}); + +test("direct chaining", () => { + util.testFunction` + const { promise, resolve } = defer(); + + promise + .then(data => { + log("resolving then1", data); + return "then 1 data"; + }).then(data => { + log("resolving then2", data); + throw "test throw"; + }).catch(reason => { + log("handling catch", reason); + }); + + resolve("test data"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + "resolving then1", + "test data", + "resolving then2", + "then 1 data", + "handling catch", + "test throw", + ]); +}); + +describe("finally behaves same as then/catch", () => { + const thenCatchPromise = ` + const { promise, resolve, reject } = defer(); + promise + .then(data => { + log("do something", data); + log("final code"); + }) + .catch(reason => { + log("handling error", reason); + log("final code"); + }); + `; + + const finallyPromise = ` + const { promise, resolve, reject } = defer(); + promise + .then(data => { + log("do something", data); + }) + .catch(reason => { + log("handling error", reason); + }) + .finally(() => { + log("final code"); + }); + `; + + test("when resolving", () => { + const thenResult = util.testFunction` + ${thenCatchPromise} + resolve("test data"); + return allLogs; + ` + .setTsHeader(promiseTestLib) + .getLuaExecutionResult(); + + const finallyResult = util.testFunction` + ${finallyPromise} + resolve("test data"); + return allLogs; + ` + .setTsHeader(promiseTestLib) + .getLuaExecutionResult(); + + expect(finallyResult).toEqual(thenResult); + }); + + test("when rejecting", () => { + const thenResult = util.testFunction` + ${thenCatchPromise} + reject("test rejection reason"); + return allLogs; + ` + .setTsHeader(promiseTestLib) + .getLuaExecutionResult(); + + const finallyResult = util.testFunction` + ${finallyPromise} + reject("test rejection reason"); + return allLogs; + ` + .setTsHeader(promiseTestLib) + .getLuaExecutionResult(); + + expect(finallyResult).toEqual(thenResult); + }); +}); + +test("example: asynchronous web request", () => { + const testHarness = ` + interface UserData { name: string, age: number} + const requests = new Map void>(); + function getUserData(id: number, callback: (userData: UserData) => void) { + requests.set(id, callback); + } + function emulateRequestReturn(id: number, data: UserData) { + requests.get(id)!(data); + } + `; + + util.testFunction` + // Wrap function getUserData(id: number, callback: (userData: UserData) => void) into a Promise + function getUserDataAsync(id: number): Promise { + return new Promise(resolve => { + getUserData(id, resolve); + }); + } + + const user1DataPromise = getUserDataAsync(1); + const user2DataPromise = getUserDataAsync(2); + + user1DataPromise.then(() => log("received data for user 1")); + user2DataPromise.then(() => log("received data for user 2")); + + const allDataPromise = Promise.all([user1DataPromise, user2DataPromise]); + + allDataPromise.then(([user1data, user2data]) => { + log("all requests completed", user1data, user2data); + }); + + emulateRequestReturn(2, { name: "bar", age: 42 }); + emulateRequestReturn(1, { name: "foo", age: 35 }); + + return allLogs; + ` + .setTsHeader(testHarness + promiseTestLib) + .expectToEqual([ + "received data for user 2", + "received data for user 1", + "all requests completed", + { + name: "foo", + age: 35, + }, + { + name: "bar", + age: 42, + }, + ]); +}); + +test("promise is instanceof promise", () => { + util.testExpression`Promise.resolve(4) instanceof Promise`.expectToMatchJsResult(); +}); + +test("chained then on resolved promise", () => { + util.testFunction` + Promise.resolve("result1").then(undefined, () => {}).then(value => log(value)); + Promise.resolve("result2").then(value => "then1", () => {}).then(value => log(value)); + Promise.resolve("result3").then(value => undefined, () => {}).then(value => log(value ?? "undefined")); + Promise.resolve("result4").then(value => "then2").then(value => [value, "then3"]).then(([v1, v2]) => log(v1, v2)); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["result1", "then1", "undefined", "then2", "then3"]); +}); + +test("chained catch on rejected promise", () => { + util.testFunction` + Promise.reject("reason1").then(() => {}).then(v => log("resolved", v), reason => log("rejected", reason)); + Promise.reject("reason2").then(() => {}, () => "reason3").then(v => log("resolved", v)); + Promise.reject("reason4").then(() => {}, () => undefined).then(v => log("resolved", v ?? "undefined")); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["rejected", "reason1", "resolved", "reason3", "resolved", "undefined"]); +}); + +// Issue 2 from https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1105 +test("catch after then catches rejected promise", () => { + util.testFunction` + Promise.reject('test error') + .then(result => { + log("then", result); + }) + .catch(e => { + log("catch", e); + }) + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["catch", "test error"]); +}); + +test("promise unwraps resolved promise result", () => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(v => log(v)); + + resolve(Promise.resolve("result")); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["result"]); +}); + +test("resolving promise with rejected promise rejects the promise", () => { + util.testFunction` + const { promise, resolve } = defer(); + promise.catch(err => log(err)); + + resolve(Promise.reject("reject")); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["reject"]); +}); + +test("resolving promise with pending promise will keep pending until promise2 resolved", () => { + util.testFunction` + const { promise, resolve } = defer(); + promise.then(v => log("promise 1", v)); + + const { promise: promise2, resolve: resolve2 } = defer(); + promise2.then(v => log("promise 2", v)); + + resolve(promise2); + resolve2("result"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 2", "result", "promise 1", "result"]); +}); + +test("resolving promise with pending promise will keep pending until promise2 rejects", () => { + util.testFunction` + const { promise, resolve } = defer(); + promise.catch(v => log("promise 1", v)); + + const { promise: promise2, reject: reject2 } = defer(); + promise2.catch(v => log("promise 2", v)); + + resolve(promise2); + reject2("rejection"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 2", "rejection", "promise 1", "rejection"]); +}); + +describe("Promise.resolve", () => { + test("returns the given resolved promise", () => { + util.testFunction` + let result = 0; + Promise.resolve(Promise.resolve(4)) + .then((value) => result = value); + return result; + `.expectToEqual(4); + }); + + test("returns the given rejected promise", () => { + util.testFunction` + let result = 0; + Promise.resolve(Promise.reject(4)) + .catch((value) => result = value); + return result; + `.expectToEqual(4); + }); +}); + +describe("Promise.all", () => { + test("resolves once all arguments are resolved", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + const promise = Promise.all([promise1, promise2, promise3]); + promise.then(([result1, result2, result3]) => { + log(result1, result2, result3); + }); + + resolve1("promise 1 result!"); + resolve2("promise 2 result!"); + resolve3("promise 3 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 1 result!", "promise 2 result!", "promise 3 result!"]); + }); + + test("rejects on first rejection", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, reject: reject2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + const promise = Promise.all([promise1, promise2, promise3]); + promise.then( + ([result1, result2, result3]) => { + log(result1, result2, result3); + }, + reason => { + log(reason); + } + ); + + resolve1("promise 1 result!"); + reject2("promise 2 rejects!"); + resolve3("promise 3 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 2 rejects!"]); + }); + + test("handles already-resolved promises", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + + const promise = Promise.all([promise1, Promise.resolve("already resolved!")]); + promise.then(([result1, result2]) => { + log(result1, result2); + }); + + resolve1("promise 1 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 1 result!", "already resolved!"]); + }); + + test("handles non-promise data", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + + const promise = Promise.all([42, promise1, "foo"]); + promise.then(([result1, result2, result3]) => { + log(result1, result2, result3); + }); + + resolve1("promise 1 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([42, "promise 1 result!", "foo"]); + }); + + test("returns already-resolved promise if no pending promises in arguments", () => { + util.testFunction` + const { state, value } = Promise.all([42, Promise.resolve("already resolved!"), "foo"]) as any; + return { state, value }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: [42, "already resolved!", "foo"], + }); + }); + + test("returns already-rejected promise if already rejected promise in arguments", () => { + util.testFunction` + const { state, rejectionReason } = Promise.all([42, Promise.reject("already rejected!")]) as any; + return { state, rejectionReason }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "already rejected!", + }); + }); +}); + +describe("Promise.allSettled", () => { + test("resolves once all arguments are resolved", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + const promise = Promise.allSettled([promise1, promise2, promise3]); + promise.then(([result1, result2, result3]) => { + log(result1, result2, result3); + }); + + resolve3("promise 3 result!"); + resolve1("promise 1 result!"); + resolve2("promise 2 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { status: "fulfilled", value: "promise 1 result!" }, + { status: "fulfilled", value: "promise 2 result!" }, + { status: "fulfilled", value: "promise 3 result!" }, + ]); + }); + + test("resolves once all arguments are rejected", () => { + util.testFunction` + const { promise: promise1, reject: reject1 } = defer(); + const { promise: promise2, reject: reject2 } = defer(); + const { promise: promise3, reject: reject3 } = defer(); + + const promise = Promise.allSettled([promise1, promise2, promise3]); + promise.then(([result1, result2, result3]) => { + log(result1, result2, result3); + }); + + reject2("promise 2 rejected!"); + reject1("promise 1 rejected!"); + reject3("promise 3 rejected!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { status: "rejected", reason: "promise 1 rejected!" }, + { status: "rejected", reason: "promise 2 rejected!" }, + { status: "rejected", reason: "promise 3 rejected!" }, + ]); + }); + + test("resolves once all arguments are rejected or resolved", () => { + util.testFunction` + const { promise: promise1, reject: reject1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, reject: reject3 } = defer(); + + const promise = Promise.allSettled([promise1, promise2, promise3]); + promise.then(([result1, result2, result3]) => { + log(result1, result2, result3); + }); + + resolve2("promise 2 resolved!"); + reject1("promise 1 rejected!"); + reject3("promise 3 rejected!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { status: "rejected", reason: "promise 1 rejected!" }, + { status: "fulfilled", value: "promise 2 resolved!" }, + { status: "rejected", reason: "promise 3 rejected!" }, + ]); + }); + + test("handles already resolved promises", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const returnedPromise = Promise.allSettled([Promise.resolve("already resolved"), promise]); + returnedPromise.then(([result1, result2]) => { + log(result1, result2); + }); + + resolve("promise resolved!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { status: "fulfilled", value: "already resolved" }, + { status: "fulfilled", value: "promise resolved!" }, + ]); + }); + + test("handles already rejected promises", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const returnedPromise = Promise.allSettled([Promise.reject("already rejected"), promise]); + returnedPromise.then(([result1, result2]) => { + log(result1, result2); + }); + + resolve("promise resolved!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { status: "rejected", reason: "already rejected" }, + { status: "fulfilled", value: "promise resolved!" }, + ]); + }); + + test("handles literal arguments", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const returnedPromise = Promise.allSettled(["my literal", promise]); + returnedPromise.then(([result1, result2]) => { + log(result1, result2); + }); + + resolve("promise resolved!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { status: "fulfilled", value: "my literal" }, + { status: "fulfilled", value: "promise resolved!" }, + ]); + }); + + test("returns resolved promise for empty argument list", () => { + util.testFunction` + const { state, value } = Promise.allSettled([]) as any; + return { state, value }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: [], + }); + }); +}); + +describe("Promise.any", () => { + test("resolves once first promise resolves", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + const promise = Promise.any([promise1, promise2, promise3]); + promise.then(data => { + log(data); + }); + + resolve2("promise 2 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 2 result!"]); + }); + + test("rejects once all promises reject", () => { + util.testFunction` + const { promise: promise1, reject: reject1 } = defer(); + const { promise: promise2, reject: reject2 } = defer(); + const { promise: promise3, reject: reject3 } = defer(); + + const promise = Promise.any([promise1, promise2, promise3]); + promise.catch(reason => { + log(reason); + }); + + reject2("promise 2 rejected!"); + reject3("promise 3 rejected!"); + reject1("promise 1 rejected!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { + name: "AggregateError", + message: "All Promises rejected", + errors: ["promise 2 rejected!", "promise 3 rejected!", "promise 1 rejected!"], + }, + ]); + }); + + test("handles already rejected promises", () => { + util.testFunction` + const { promise: promise1, reject: reject1 } = defer(); + const { promise: promise2, reject: reject2 } = defer(); + + const promise = Promise.any([promise1, Promise.reject("already rejected!"), promise2]); + promise.catch(reason => { + log(reason); + }); + + reject2("promise 2 rejected!"); + reject1("promise 1 rejected!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual([ + { + name: "AggregateError", + message: "All Promises rejected", + errors: ["already rejected!", "promise 2 rejected!", "promise 1 rejected!"], + }, + ]); + }); + + test("rejects if iterable is empty", () => { + util.testFunction` + const { state, rejectionReason } = Promise.any([]) as any; + return { state, rejectionReason }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "No promises to resolve with .any()", + }); + }); + + test("immediately resolves with literal", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const { state, value } = Promise.any([promise, "my literal"]) as any; + return { state, value }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "my literal", + }); + }); + + test("immediately resolves with resolved promise", () => { + util.testFunction` + const { promise, resolve } = defer(); + + const { state, value } = Promise.any([promise, Promise.resolve("my resolved promise")]) as any; + return { state, value }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "my resolved promise", + }); + }); +}); + +describe("Promise.race", () => { + test("resolves once first promise resolves", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + const promise = Promise.race([promise1, promise2, promise3]); + promise.then(data => { + log(data); + }); + + resolve2("promise 2 result!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 2 result!"]); + }); + + test("rejects once first promise rejects", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, reject: reject2 } = defer(); + const { promise: promise3, resolve: resolve3 } = defer(); + + const promise = Promise.race([promise1, promise2, promise3]); + promise.catch(reason => { + log(reason); + }); + + reject2("promise 2 rejected!"); + + return allLogs; + ` + .setTsHeader(promiseTestLib) + .expectToEqual(["promise 2 rejected!"]); + }); + + test("returns resolved promise if arguments contain resolved promise", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + + const { state, value } = Promise.race([promise1, Promise.resolve("already resolved!"), promise2]) as any; + return { state, value }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "already resolved!", + }); + }); + + test("returns resolved promise if arguments contain literal", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + + const { state, value } = Promise.race([promise1, "my literal", promise2]) as any; + return { state, value }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 1, // __TS__PromiseState.Fulfilled + value: "my literal", + }); + }); + + test("returns rejected promise if arguments contain rejected promise", () => { + util.testFunction` + const { promise: promise1, resolve: resolve1 } = defer(); + const { promise: promise2, resolve: resolve2 } = defer(); + + const { state, rejectionReason } = Promise.race([promise1, Promise.reject("already rejected!"), promise2]) as any; + return { state, rejectionReason }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 2, // __TS__PromiseState.Rejected + rejectionReason: "already rejected!", + }); + }); + + test("returns forever pending promise if argument array is empty", () => { + util.testFunction` + const { state } = Promise.race([]) as any; + return { state }; + ` + .setTsHeader(promiseTestLib) + .expectToEqual({ + state: 0, // __TS__PromiseState.Pending + }); + }); +}); diff --git a/test/unit/builtins/set.spec.ts b/test/unit/builtins/set.spec.ts new file mode 100644 index 000000000..a03f27ab2 --- /dev/null +++ b/test/unit/builtins/set.spec.ts @@ -0,0 +1,421 @@ +import * as util from "../../util"; + +test("set constructor", () => { + util.testFunction` + let myset = new Set(); + return myset.size; + `.expectToMatchJsResult(); +}); + +test("set iterable constructor", () => { + util.testFunction` + let myset = new Set(["a", "b"]); + return myset.has("a") || myset.has("b"); + `.expectToMatchJsResult(); +}); + +test("set iterable constructor set", () => { + util.testFunction` + let myset = new Set(new Set(["a", "b"])); + return myset.has("a") || myset.has("b"); + `.expectToMatchJsResult(); +}); + +test("set add", () => { + util.testFunction` + let myset = new Set(); + myset.add("a"); + return myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set clear", () => { + util.testFunction` + let myset = new Set(["a", "b"]); + myset.clear(); + return { size: myset.size, has: !myset.has("a") && !myset.has("b") }; + `.expectToMatchJsResult(); +}); + +test("set delete", () => { + util.testFunction` + let myset = new Set(["a", "b"]); + myset.delete("a"); + return myset.has("b") && !myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set entries", () => { + util.testFunction` + let myset = new Set([5, 6, 7]); + let count = 0; + for (const [key, value] of myset.entries()) { count += key + value; } + return count; + `.expectToMatchJsResult(); +}); + +test("set foreach", () => { + util.testFunction` + let myset = new Set([2, 3, 4]); + let count = 0; + myset.forEach(i => { count += i; }); + return count; + `.expectToMatchJsResult(); +}); + +test("set foreach keys", () => { + util.testFunction` + let myset = new Set([2, 3, 4]); + let count = 0; + myset.forEach((value, key) => { count += key; }); + return count; + `.expectToMatchJsResult(); +}); + +test("set has", () => { + util.testFunction` + let myset = new Set(["a", "c"]); + return myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set has after deleting keys", () => { + util.testFunction` + let myset = new Set(["a", "c"]); + const results = [myset.has("c")]; + myset.delete("c"); + results.push(myset.has("c")); + myset.delete("a"); + results.push(myset.has("a")) + return results; + `.expectToMatchJsResult(); +}); + +test("set has false", () => { + util.testFunction` + let myset = new Set(); + return myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set has null", () => { + util.testFunction` + let myset = new Set(["a", "c"]); + return myset.has(null); + `.expectToMatchJsResult(); +}); + +test("set keys", () => { + util.testFunction` + let myset = new Set([5, 6, 7]); + let count = 0; + for (const key of myset.keys()) { count += key; } + return count; + `.expectToMatchJsResult(); +}); + +test("set values", () => { + util.testFunction` + let myset = new Set([5, 6, 7]); + let count = 0; + for (const value of myset.values()) { count += value; } + return count; + `.expectToMatchJsResult(); +}); + +test.each([ + "let m = new Set()", + "let m = new Set(); m.add(1)", + "let m = new Set([1, 2])", + "let m = new Set([1, 2]); m.clear()", + "let m = new Set([1, 2]); m.delete(2)", +])("set size (%p)", code => { + util.testFunction`${code}; return m.size`.expectToMatchJsResult(); +}); + +const iterationMethods = ["entries", "keys", "values"]; +describe.each(iterationMethods)("set.%s() preserves insertion order", iterationMethod => { + test("basic", () => { + util.testFunction` + const myset = new Set(); + + myset.add("x"); + myset.add("a"); + myset.add(4); + myset.add("b"); + myset.add(1); + myset.add("a"); + + myset.delete("b"); + + return [...myset.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); + + test("after removing last", () => { + util.testFunction` + const myset = new Set(); + + myset.add("x"); + myset.add("a"); + myset.add(4); + + myset.delete(4); + + return [...myset.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); + + test("after removing first", () => { + util.testFunction` + const myset = new Set(); + + myset.add("x"); + myset.add("a"); + myset.add(4); + + myset.delete("x"); + + return [...myset.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); + + test("after removing all", () => { + util.testFunction` + const myset = new Set(); + + myset.add("x"); + myset.add("a"); + + myset.delete("a"); + myset.delete("x"); + + return [...myset.${iterationMethod}()]; + `.expectToMatchJsResult(); + }); +}); + +test("instanceof Set without creating set", () => { + util.testFunction` + const myset = 3 as any; + return myset instanceof Set; + `.expectToMatchJsResult(); +}); + +describe("new ECMAScript Set methods", () => { + test("union", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6]); + const set2 = new Set([4,5,6,7,8,9]); + + const intersection = set1.union(set2); + return [...intersection]; + `.expectToEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + + test("union with empty sets", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6]); + const set2 = new Set([]); + + const intersection = set1.union(set2); + return [...intersection]; + `.expectToEqual([1, 2, 3, 4, 5, 6]); + + util.testFunction` + const set1 = new Set([]); + const set2 = new Set([4,5,6,7,8,9]); + + const intersection = set1.union(set2); + return [...intersection]; + `.expectToEqual([4, 5, 6, 7, 8, 9]); + }); + + test("intersection", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6]); + const set2 = new Set([4,5,6,7,8,9]); + + const intersection = set1.intersection(set2); + return [...intersection]; + `.expectToEqual([4, 5, 6]); + }); + + test("intersection with empty sets", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6]); + const set2 = new Set([]); + + const intersection = set1.intersection(set2); + return [...intersection]; + `.expectToEqual([]); + + util.testFunction` + const set1 = new Set([]); + const set2 = new Set([4,5,6,7,8,9]); + + const intersection = set1.intersection(set2); + return [...intersection]; + `.expectToEqual([]); + }); + + test("difference", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6]); + const set2 = new Set([4,5,6,7,8,9]); + + const intersection = set1.difference(set2); + return [...intersection]; + `.expectToEqual([1, 2, 3]); + }); + + test("symmetricDifference", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6]); + const set2 = new Set([4,5,6,7,8,9]); + + const intersection = set1.symmetricDifference(set2); + return [...intersection]; + `.expectToEqual([1, 2, 3, 7, 8, 9]); + }); + + test("isSubsetOf", () => { + util.testFunction` + const set1 = new Set([3,4,5,6]); + const set2 = new Set([1,2,3,4,5,6,7,8,9]); + + return { + set1SubsetOfSet2: set1.isSubsetOf(set2), + set2SubsetOfSet1: set2.isSubsetOf(set1), + }; + `.expectToEqual({ + set1SubsetOfSet2: true, + set2SubsetOfSet1: false, + }); + }); + + test("isSubsetOf equal", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6,7,8,9]); + const set2 = new Set([1,2,3,4,5,6,7,8,9]); + + return { + set1SubsetOfSet2: set1.isSubsetOf(set2), + set2SubsetOfSet1: set2.isSubsetOf(set1), + }; + `.expectToEqual({ + set1SubsetOfSet2: true, + set2SubsetOfSet1: true, + }); + }); + + test("isSubsetOf empty", () => { + util.testFunction` + const set1 = new Set([]); + const set2 = new Set([1,2,3]); + + return { + set1SubsetOfSet2: set1.isSubsetOf(set2), + set2SubsetOfSet1: set2.isSubsetOf(set1), + }; + `.expectToEqual({ + set1SubsetOfSet2: true, + set2SubsetOfSet1: false, + }); + }); + + test("isSupersetOf", () => { + util.testFunction` + const set1 = new Set([3,4,5,6]); + const set2 = new Set([1,2,3,4,5,6,7,8,9]); + + return { + set1SupersetOfSet2: set1.isSupersetOf(set2), + set2SupersetOfSet1: set2.isSupersetOf(set1), + }; + `.expectToEqual({ + set1SupersetOfSet2: false, + set2SupersetOfSet1: true, + }); + }); + + test("isSupersetOf equal", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6,7,8,9]); + const set2 = new Set([1,2,3,4,5,6,7,8,9]); + + return { + set1SupersetOfSet2: set1.isSupersetOf(set2), + set2SupersetOfSet1: set2.isSupersetOf(set1), + }; + `.expectToEqual({ + set1SupersetOfSet2: true, + set2SupersetOfSet1: true, + }); + }); + + test("isSupersetOf empty", () => { + util.testFunction` + const set1 = new Set([]); + const set2 = new Set([1,2,3]); + + return { + set1SupersetOfSet2: set1.isSupersetOf(set2), + set2SupersetOfSet1: set2.isSupersetOf(set1), + }; + `.expectToEqual({ + set1SupersetOfSet2: false, + set2SupersetOfSet1: true, + }); + }); + + test("isDisjointFrom", () => { + util.testFunction` + const set1 = new Set([3,4,5,6]); + const set2 = new Set([7,8,9]); + const set3 = new Set([1,2,3,4]); + + return { + set1DisjointFromSet2: set1.isDisjointFrom(set2), + set2DisjointFromSet1: set2.isDisjointFrom(set1), + set1DisjointFromSet3: set1.isDisjointFrom(set3), + set3DisjointFromSet1: set3.isDisjointFrom(set1), + }; + `.expectToEqual({ + set1DisjointFromSet2: true, + set2DisjointFromSet1: true, + set1DisjointFromSet3: false, + set3DisjointFromSet1: false, + }); + }); + + test("isDisjointFrom equal", () => { + util.testFunction` + const set1 = new Set([1,2,3,4,5,6,7,8,9]); + const set2 = new Set([1,2,3,4,5,6,7,8,9]); + + return { + set1DisjointFromSet2: set1.isDisjointFrom(set2), + set2DisjointFromSet1: set2.isDisjointFrom(set1), + }; + `.expectToEqual({ + set1DisjointFromSet2: false, + set2DisjointFromSet1: false, + }); + }); + + test("isDisjointFrom empty", () => { + util.testFunction` + const set1 = new Set([]); + const set2 = new Set([1,2,3]); + + return { + set1DisjointFromSet2: set1.isDisjointFrom(set2), + set2DisjointFromSet1: set2.isDisjointFrom(set1), + }; + `.expectToEqual({ + set1DisjointFromSet2: true, + set2DisjointFromSet1: true, + }); + }); +}); diff --git a/test/unit/builtins/string.spec.ts b/test/unit/builtins/string.spec.ts new file mode 100644 index 000000000..977944e55 --- /dev/null +++ b/test/unit/builtins/string.spec.ts @@ -0,0 +1,425 @@ +import { LuaLibImportKind } from "../../../src"; +import * as util from "../../util"; + +test("Supported lua string function", () => { + const tsHeader = ` + declare global { + interface String { + upper(): string; + } + } + `; + + util.testExpression`"test".upper()`.setTsHeader(tsHeader).expectToEqual("TEST"); +}); + +test("string.toString()", () => { + util.testExpression`"test".toString()`.expectToEqual("test"); +}); + +test.each([[], [65], [65, 66], [65, 66, 67]])("String.fromCharCode (%p)", (...args) => { + util.testExpression`String.fromCharCode(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + { a: 12, b: 23, c: 43 }, + { a: "test", b: "hello", c: "bye" }, + { a: "test", b: 42, c: "bye" }, + { a: "test", b: 42, c: 12 }, + { a: "test", b: 42, c: true }, + { a: false, b: 42, c: 12 }, +])("String Concat Operator (%p)", ({ a, b, c }) => { + util.testFunctionTemplate` + let a = ${a}; + let b = ${b}; + let c = ${c}; + return a + " " + b + " test " + c; + `.expectToMatchJsResult(); +}); + +test.each([ + { input: "01234", index: 0 }, + { input: "01234", index: 1 }, + { input: "01234", index: 4 }, + { input: "01234", index: 5 }, + { input: "01234", index: -1 }, + { input: "01234", index: 100 }, + { input: "01234", index: NaN }, + { input: "", index: 0 }, +])("string index (%p)", ({ input, index }) => { + util.testExpressionTemplate`${input}[${index}]`.expectToMatchJsResult(); +}); + +test("string index (side effect)", () => { + util.testFunction` + let i = 0; + const mystring = "abc"; + return mystring[i++]; + `.expectToMatchJsResult(); +}); + +describe.each(["replace", "replaceAll"])("string.%s", method => { + const testCases = [ + { inp: "hello test", searchValue: "", replaceValue: "" }, + { inp: "hello test", searchValue: "", replaceValue: "_" }, + { inp: "hello test", searchValue: " ", replaceValue: "" }, + { inp: "hello test", searchValue: "hello", replaceValue: "" }, + { inp: "hello test", searchValue: "test", replaceValue: "" }, + { inp: "hello test", searchValue: "test", replaceValue: "world" }, + { inp: "hello test", searchValue: "test", replaceValue: "%world" }, + { inp: "hello test", searchValue: "test", replaceValue: "." }, + { inp: "hello %test", searchValue: "test", replaceValue: "world" }, + { inp: "hello %test", searchValue: "%test", replaceValue: "world" }, + { inp: "hello test.", searchValue: ".", replaceValue: "$" }, + { inp: "aaa", searchValue: "a", replaceValue: "b" }, + ]; + + test.each(testCases)("string replacer %p", ({ inp, searchValue, replaceValue }) => { + util.testExpression`"${inp}${inp}".${method}(${util.formatCode( + searchValue, + replaceValue + )})`.expectToMatchJsResult(); + }); + + test.each(testCases)("function replacer %p", ({ inp, searchValue, replaceValue }) => { + util.testFunction` + const result = { + args: [], + string: "" + } + function replacer(...args: any[]): string { + result.args.push(...args) + return ${util.formatCode(replaceValue)} + } + result.string = "${inp}${inp}".${method}(${util.formatCode(searchValue)}, replacer) + return result + `.expectToMatchJsResult(); + }); +}); + +test.each([ + ["", ""], + ["hello", "test"], + ["hello", "test", "bye"], + ["hello", 42], + [42, "hello"], +])("string + (%p)", (...elements: any[]) => { + util.testExpression(elements.map(e => util.formatCode(e)).join(" + ")).expectToMatchJsResult(); +}); + +test.each([ + { str: "", args: ["", ""] }, + { str: "hello", args: ["test"] }, + { str: "hello", args: [] }, + { str: "hello", args: ["test", "bye"] }, +])("string.concat (%p)", ({ str, args }) => { + util.testExpression`"${str}".concat(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", searchValue: "" }, + { inp: "hello test", searchValue: "t" }, + { inp: "hello test", searchValue: "h" }, + { inp: "hello test", searchValue: "invalid" }, + { inp: "hello.test", searchValue: "." }, +])("string.indexOf (%p)", ({ inp, searchValue }) => { + util.testExpressionTemplate`${inp}.indexOf(${searchValue})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", searchValue: "t", offset: 5 }, + { inp: "hello test", searchValue: "t", offset: 6 }, + { inp: "hello test", searchValue: "t", offset: 7 }, + { inp: "hello test", searchValue: "h", offset: 4 }, + { inp: "00100", searchValue: "1", offset: -1 }, + { inp: "00100", searchValue: "1", offset: -2 }, + { inp: "01010", searchValue: "1", offset: -3 }, +])("string.indexOf with offset (%p)", ({ inp, searchValue, offset }) => { + util.testExpressionTemplate`${inp}.indexOf(${searchValue}, ${offset})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", searchValue: "t", x: 4, y: 3 }, + { inp: "hello test", searchValue: "h", x: 3, y: 4 }, +])("string.indexOf with offset expression (%p)", ({ inp, searchValue, x, y }) => { + util.testExpressionTemplate`${inp}.indexOf(${searchValue}, 2 > 1 && ${x} || ${y})`.expectToMatchJsResult(); +}); + +const stringPartCases = [ + { inp: "0123456789", args: [0] }, + { inp: "0123456789", args: [0, 0] }, + { inp: "0123456789", args: [1] }, + { inp: "0123456789", args: [1, 1] }, + { inp: "0123456789", args: [1, 5] }, + { inp: "0123456789", args: [5, 1] }, + { inp: "0123456789", args: [1, 100] }, + { inp: "0123456789", args: [100, 1] }, + { inp: "0123456789", args: [100, 101] }, + { inp: "0123456789", args: [-3] }, + { inp: "0123456789", args: [1, -1] }, + { inp: "0123456789", args: [-5, -2] }, + { inp: "0123456789", args: [NaN, 3] }, + { inp: "0123456789", args: [3, NaN] }, +]; + +test.each([{ inp: "0123456789", args: [] }, { inp: "0123456789", args: [undefined, 5] }, ...stringPartCases])( + "string.slice (%p)", + ({ inp, args }) => { + util.testExpression`"${inp}".slice(${util.formatCode(...args)})`.expectToMatchJsResult(); + } +); + +test.each(stringPartCases)("string.substring (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".substring(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", start: 1, ignored: 0 }, + { inp: "hello test", start: 3, ignored: 0, end: 5 }, +])("string.substring with expression (%p)", ({ inp, start, ignored, end }) => { + const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); + util.testExpression`"${inp}".substring(${paramStr})`.expectToMatchJsResult(); +}); + +test.each(stringPartCases)("string.substr (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".substr(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", start: 1, ignored: 0 }, + { inp: "hello test", start: 3, ignored: 0, end: 2 }, +])("string.substr with expression (%p)", ({ inp, start, ignored, end }) => { + const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); + util.testExpression` + "${inp}".substr(${paramStr}) + `.expectToMatchJsResult(); +}); + +test.each(["", "h", "hello"])("string.length (%p)", input => { + util.testExpressionTemplate`${input}.length`.expectToMatchJsResult(); +}); + +test.each(["hello TEST"])("string.toLowerCase (%p)", inp => { + util.testExpressionTemplate`${inp}.toLowerCase()`.expectToMatchJsResult(); +}); + +test.each(["hello test"])("string.toUpperCase (%p)", inp => { + util.testExpressionTemplate`${inp}.toUpperCase()`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", separator: "" }, + { inp: "hello test", separator: " " }, + { inp: "hello test", separator: "h" }, + { inp: "hello test", separator: "t" }, + { inp: "hello test", separator: "l" }, + { inp: "hello test", separator: "invalid" }, + { inp: "hello test", separator: "hello test" }, +])("string.split (%p)", ({ inp, separator }) => { + util.testExpressionTemplate`${inp}.split(${separator})`.expectToMatchJsResult(); +}); + +test("string.split inline", () => { + util.testExpression`"a, b, c".split(",")` + .setOptions({ luaLibImport: LuaLibImportKind.Inline }) + .expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1009 +test("string.split inline empty separator", () => { + util.testExpression`"a, b, c".split("")` + .setOptions({ luaLibImport: LuaLibImportKind.Inline }) + .expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", index: 0 }, + { inp: "hello test", index: 1 }, + { inp: "hello test", index: 2 }, + { inp: "hello test", index: 3 }, + { inp: "hello test", index: 99 }, + { inp: "hello test", index: -1 }, + { inp: "hello test", index: -5 }, + { inp: "hello test", index: -99 }, + { inp: "hello test", index: NaN }, +])("string.charAt (%p)", ({ inp, index }) => { + util.testExpressionTemplate`${inp}.charAt(${index})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", index: 0 }, + { inp: "hello test", index: 1 }, + { inp: "hello test", index: 2 }, + { inp: "hello test", index: 3 }, + { inp: "hello test", index: 99 }, + { inp: "hello test", index: -1 }, + { inp: "hello test", index: -5 }, + { inp: "hello test", index: -99 }, + { inp: "hello test", index: NaN }, +])("string.charCodeAt (%p)", ({ inp, index }) => { + util.testExpressionTemplate`${inp}.charCodeAt(${index})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", index: 1, ignored: 0 }, + { inp: "hello test", index: 1, ignored: 2 }, + { inp: "hello test", index: 3, ignored: 2 }, + { inp: "hello test", index: 3, ignored: 99 }, +])("string.charAt with expression (%p)", ({ inp, index, ignored }) => { + util.testExpressionTemplate`${inp}.charAt(2 > 1 && ${index} || ${ignored})`.expectToMatchJsResult(); +}); + +test.each<{ inp: string; args: Parameters }>([ + { inp: "hello test", args: [""] }, + { inp: "hello test", args: ["hello"] }, + { inp: "HELLO test", args: ["hello"] }, + { inp: "hello test", args: ["test"] }, + { inp: "hello test", args: ["test", 6] }, +])("string.startsWith (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".startsWith(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each<{ inp: string; args: Parameters }>([ + { inp: "hello test", args: [""] }, + { inp: "hello test", args: ["test"] }, + { inp: "hello TEST", args: ["test"] }, + { inp: "hello test", args: ["hello"] }, + { inp: "hello test", args: ["hello", 5] }, +])("string.endsWith (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".endsWith(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each<{ inp: string; args: Parameters }>([ + { inp: "hello test", args: [""] }, + { inp: "hello test", args: ["test"] }, + { inp: "HELLO TEST", args: ["test"] }, + { inp: "hello test", args: ["hello"] }, + { inp: "HELLO TEST", args: ["hello"] }, + { inp: "hello test", args: ["hello", 5] }, + { inp: "hello test", args: ["test", 6] }, +])("string.includes (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".includes(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", count: 0 }, + { inp: "hello test", count: 1 }, + { inp: "hello test", count: 2 }, + { inp: "hello test", count: 1.1 }, + { inp: "hello test", count: 1.5 }, + { inp: "hello test", count: 1.9 }, +])("string.repeat (%p)", ({ inp, count }) => { + util.testExpression`"${inp}".repeat(${count})`.expectToMatchJsResult(); +}); + +const padCases = [ + { inp: "foo", args: [0] }, + { inp: "foo", args: [3] }, + { inp: "foo", args: [5] }, + { inp: "foo", args: [4, " "] }, + { inp: "foo", args: [10, " "] }, + { inp: "foo", args: [5, "1234"] }, + { inp: "foo", args: [5.9, "1234"] }, + { inp: "foo", args: [NaN] }, +]; + +test.each(padCases)("string.padStart (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".padStart(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each(padCases)("string.padEnd (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".padEnd(${util.formatCode(...args)})`.expectToMatchJsResult(); +}); + +test.each([ + "function generic(string: T)", + "type StringType = string; function generic(string: T)", +])("string constrained generic foreach (%p)", signature => { + util.testFunction` + ${signature}: number { + return string.length; + } + return generic("string"); + `.expectToMatchJsResult(); +}); + +const trimTestCases = [ + "", + " ", + "\t", + "\t \t", + " foo ", + "\tfoo\t", + "\ffoo\f", + "\vfoo\v", + "\uFEFFFoo\uFEFF", + "\xA0Foo\xA0", + " \t foo \t ", + " foo bar ", + "\r\nfoo\n\r\n", + "\r\nfoo\nbar\n\r\n", +]; +describe.each(["trim", "trimEnd", "trimRight", "trimStart", "trimLeft"])("string.%s", trim => { + test.each(trimTestCases)("matches JS result (%p)", testString => { + util.testExpression`${util.formatCode(testString)}.${trim}()`.expectToMatchJsResult(); + }); +}); + +test("string intersected method", () => { + util.testFunction` + type Vector = string & { abc(): Vector }; + return ({ abc: () => "a" } as Vector).abc(); + `.expectToMatchJsResult(); +}); + +test("tostring number with String constructor", () => { + util.testFunction` + const n = 123 + return "abc:" + String(n); + `.expectToEqual("abc:123"); +}); + +test("tostring table with String constructor", () => { + const result = util.testFunction` + const t = {} + return "abc:" + String(t); + `.getLuaExecutionResult(); + expect(result).toContain("abc:table: 0x"); +}); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(['"foo"', "undefined"])("prototype call on nullable string (%p)", value => { + util.testFunction` + function toUpper(str?: string) { + return str?.toUpperCase(); + } + return toUpper(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["string | undefined", "string | null", "null | string", "null | undefined | string"])( + "prototype call on nullable string type (%p)", + type => { + util.testFunction` + function toUpper(str: ${type}) { + return str?.toUpperCase(); + } + return toUpper("foo"); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); + } +); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1406 +test("string.indexOf without arguments (#1406)", () => { + util.testExpression`"".indexOf()`.expectNoTranspileException(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1406 +test("string.repeat without arguments (#1406)", () => { + util.testExpression`"".repeat()`.expectNoTranspileException(); +}); diff --git a/test/unit/builtins/symbol.spec.ts b/test/unit/builtins/symbol.spec.ts new file mode 100644 index 000000000..3d6b35d8a --- /dev/null +++ b/test/unit/builtins/symbol.spec.ts @@ -0,0 +1,38 @@ +import * as util from "../../util"; + +test.each([undefined, 1, "name"])("symbol.toString() (%p)", description => { + util.testExpression`Symbol(${util.formatCode(description)}).toString()`.expectToMatchJsResult(); +}); + +test.each([undefined, 1, "name"])("symbol.description (%p)", description => { + // TODO: Supported since node 11 + util.testExpression`Symbol(${util.formatCode(description)}).description`.expectToEqual(description); +}); + +test("symbol uniqueness", () => { + util.testExpression`Symbol("a") === Symbol("a")`.expectToMatchJsResult(); +}); + +test("Symbol.for", () => { + // TODO: Supported since node 11 + util.testExpression('Symbol.for("name").description').expectToEqual("name"); +}); + +test("Symbol.for non-uniqueness", () => { + util.testExpression`Symbol.for("a") === Symbol.for("a")`.expectToMatchJsResult(); +}); + +test("Symbol.keyFor", () => { + util.testFunction` + const sym = Symbol.for("a"); + Symbol.for("b"); + return Symbol.keyFor(sym); + `.expectToMatchJsResult(); +}); + +test("Symbol.keyFor empty", () => { + util.testFunction` + Symbol.for("a"); + return Symbol.keyFor(Symbol()); + `.expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/weakMap.spec.ts b/test/unit/builtins/weakMap.spec.ts new file mode 100644 index 000000000..812e43622 --- /dev/null +++ b/test/unit/builtins/weakMap.spec.ts @@ -0,0 +1,104 @@ +import * as util from "../../util"; + +const initRefsTs = ` + let ref = {}; + let ref2 = () => {}; +`; + +test("weakMap constructor", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[ref, 1]]); + return mymap.get(ref); + `.expectToMatchJsResult(); +}); + +test("weakMap iterable constructor", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[ref, 1], [ref2, 2]]); + return mymap.has(ref) && mymap.has(ref2); + `.expectToMatchJsResult(); +}); + +test("weakMap iterable constructor map", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap(new Map([[ref, 1], [ref2, 2]])); + return mymap.has(ref) && mymap.has(ref2); + `.expectToMatchJsResult(); +}); + +test("weakMap delete", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[ref, true], [ref2, true]]); + mymap.delete(ref2); + return mymap.has(ref) && !mymap.has(ref2); + `.expectToMatchJsResult(); +}); + +test("weakMap get", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[ref, 1], [{}, 2]]); + return mymap.get(ref); + `.expectToMatchJsResult(); +}); + +test("weakMap get missing", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[{}, true]]); + return mymap.get({}); + `.expectToMatchJsResult(); +}); + +test("weakMap has", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[ref, true]]); + return mymap.has(ref); + `.expectToMatchJsResult(); +}); + +test("weakMap has false", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[ref, true]]); + return mymap.has(ref2); + `.expectToMatchJsResult(); +}); + +test("weakMap has null", () => { + util.testFunction` + ${initRefsTs} + let mymap = new WeakMap([[{}, true]]); + return mymap.has(null); + `.expectToMatchJsResult(); +}); + +test("weakMap set", () => { + const init = ` + ${initRefsTs} + let mymap = new WeakMap(); + mymap.set(ref, 5); + `; + + util.testFunction(init + "return mymap.has(ref);").expectToMatchJsResult(); + + util.testFunction(init + "return mymap.get(ref)").expectToMatchJsResult(); +}); + +test("weakMap has no map features (size)", () => { + util.testExpression("(new WeakMap() as any).size").expectToMatchJsResult(); +}); + +test.each(["clear()", "keys()", "values()", "entries()", "forEach(() => {})"])( + "weakMap has no map features (%p)", + call => { + const testBuilder = util.testFunction(`(new WeakMap() as any).${call}`); + const luaResult = testBuilder.getLuaExecutionResult(); + expect(luaResult.message).toContain("attempt to call a nil value"); + } +); diff --git a/test/unit/builtins/weakSet.spec.ts b/test/unit/builtins/weakSet.spec.ts new file mode 100644 index 000000000..33c5e80c3 --- /dev/null +++ b/test/unit/builtins/weakSet.spec.ts @@ -0,0 +1,70 @@ +import * as util from "../../util"; + +const initRefsTs = ` + let ref = {}; + let ref2 = () => {}; +`; + +test("weakSet constructor", () => { + util.testFunction` + ${initRefsTs} + let myset = new WeakSet([ref]); + return myset.has(ref) + `.expectToMatchJsResult(); +}); + +test("weakSet iterable constructor", () => { + util.testFunction` + ${initRefsTs} + let myset = new WeakSet([ref, ref2]); + return myset.has(ref) && myset.has(ref2); + `.expectToMatchJsResult(); +}); + +test("weakSet iterable constructor set", () => { + util.testFunction` + ${initRefsTs} + let myset = new WeakSet(new Set([ref, ref2])); + return myset.has(ref) && myset.has(ref2); + `.expectToMatchJsResult(); +}); + +test("weakSet add", () => { + util.testFunction` + ${initRefsTs} + let myset = new WeakSet(); + myset.add(ref); + return myset.has(ref); + `.expectToMatchJsResult(); +}); + +test("weakSet add different references", () => { + util.testFunction` + ${initRefsTs} + let myset = new WeakSet(); + myset.add({}); + return myset.has({}); + `.expectToMatchJsResult(); +}); + +test("weakSet delete", () => { + util.testFunction` + ${initRefsTs} + let myset = new WeakSet([ref, ref2]); + myset.delete(ref); + return myset.has(ref2) && !myset.has(ref); + `.expectToMatchJsResult(); +}); + +test("weakSet has no set features (size)", () => { + util.testExpression("(new WeakSet() as any).size").expectToMatchJsResult(); +}); + +test.each(["clear()", "keys()", "values()", "entries()", "forEach(() => {})"])( + "weakSet has no set features (%p)", + call => { + const testBuilder = util.testFunction(`(new WeakSet() as any).${call}`); + const luaResult = testBuilder.getLuaExecutionResult(); + expect(luaResult.message).toContain("attempt to call a nil value"); + } +); diff --git a/test/unit/bundle.spec.ts b/test/unit/bundle.spec.ts new file mode 100644 index 000000000..b8179363a --- /dev/null +++ b/test/unit/bundle.spec.ts @@ -0,0 +1,151 @@ +import { BuildMode, LuaLibImportKind } from "../../src"; +import * as diagnosticFactories from "../../src/transpilation/diagnostics"; +import * as util from "../util"; + +test("import module -> main", () => { + util.testBundle` + export { value } from "./module"; + ` + .addExtraFile("module.ts", "export const value = true") + .expectToEqual({ value: true }); +}); + +test("import chain export -> reexport -> main", () => { + util.testBundle` + export { value } from "./reexport"; + ` + .addExtraFile("reexport.ts", "export { value } from './export'") + .addExtraFile("export.ts", "export const value = true") + .expectToEqual({ value: true }); +}); + +test("diamond imports/exports -> reexport1 & reexport2 -> main", () => { + util.testBundle` + export { value as a } from "./reexport1"; + export { value as b } from "./reexport2"; + ` + .addExtraFile("reexport1.ts", "export { value } from './export'") + .addExtraFile("reexport2.ts", "export { value } from './export'") + .addExtraFile("export.ts", "export const value = true") + .expectToEqual({ a: true, b: true }); +}); + +test("module in directory", () => { + util.testBundle` + export { value } from "./module/module"; + ` + .addExtraFile("module/module.ts", "export const value = true") + .expectToEqual({ value: true }); +}); + +test("modules aren't ordered by name", () => { + util.testBundle` + export { value } from "./a"; + ` + .addExtraFile("a.ts", "export const value = true") + .expectToEqual({ value: true }); +}); + +test("entry point in directory", () => { + util.testBundle`` + .addExtraFile( + "main/main.ts", + ` + export { value } from "../module"; + ` + ) + .addExtraFile("module.ts", "export const value = true") + .setEntryPoint("main/main.ts") + .expectToEqual({ value: true }); +}); + +test("entry point in rootDir", () => { + util.testModule` + export { value } from "./module"; + ` + .setMainFileName("src/main.ts") + .addExtraFile("src/module.ts", "export const value = true") + .setOptions({ rootDir: "src", luaBundle: "bundle.lua", luaBundleEntry: "src/main.ts" }) + .expectToEqual({ value: true }); +}); + +test("LuaLibImportKind.Require", () => { + util.testBundle` + export const result = [1, 2]; + result.push(3); + ` + .setOptions({ luaLibImport: LuaLibImportKind.Require }) + .expectToEqual({ result: [1, 2, 3] }); +}); + +test("LuaLibImportKind.Inline generates a warning", () => { + util.testBundle` + export const result = [1, 2]; + result.push(3); + ` + .setOptions({ luaLibImport: LuaLibImportKind.Inline }) + .expectDiagnosticsToMatchSnapshot( + [diagnosticFactories.usingLuaBundleWithInlineMightGenerateDuplicateCode.code], + true + ) + .expectToEqual({ result: [1, 2, 3] }); +}); + +test("cyclic imports", () => { + util.testBundle` + import * as b from "./b"; + export const a = true; + export const valueResult = b.value; + export const lazyValueResult = b.lazyValue(); + ` + .addExtraFile( + "b.ts", + ` + import * as a from "./main"; + export const value = a.a; + export const lazyValue = () => a.a; + ` + ) + .expectToEqual(new util.ExecutionError("stack overflow")); +}); + +test("does not evaluate files multiple times", () => { + util.testBundle` + import "./countingfile"; + import "./otherfile"; + + export const count = _count; + ` + .addExtraFile( + "otherfile.ts", + ` + import "./countingfile"; + ` + ) + .addExtraFile( + "countingfile.ts", + ` + declare var _count: number | undefined; + _count = (_count ?? 0) + 1; + ` + ) + .expectToEqual({ count: 1 }); +}); + +test("no entry point", () => { + util.testBundle`` + .setOptions({ luaBundleEntry: undefined }) + .expectDiagnosticsToMatchSnapshot([diagnosticFactories.luaBundleEntryIsRequired.code], true); +}); + +test("luaEntry doesn't exist", () => { + util.testBundle`` + .setEntryPoint("entry.ts") + .expectDiagnosticsToMatchSnapshot([diagnosticFactories.couldNotFindBundleEntryPoint.code], true); +}); + +test("bundling not allowed for buildmode library", () => { + util.testBundle`` + .setOptions({ buildMode: BuildMode.Library }) + .expectDiagnosticsToMatchSnapshot([diagnosticFactories.cannotBundleLibrary.code], true); +}); diff --git a/test/unit/class.spec.ts b/test/unit/class.spec.ts deleted file mode 100644 index 5592933e9..000000000 --- a/test/unit/class.spec.ts +++ /dev/null @@ -1,808 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import * as ts from "typescript"; -import * as util from "../src/util"; -import { TranspileError } from "../../src/TranspileError"; - -export class ClassTests { - - @Test("ClassFieldInitializer") - public classFieldInitializer(): void { - const result = util.transpileAndExecute( - `class a { - field: number = 4; - } - return new a().field;` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassNumericLiteralFieldInitializer") - public classNumericLiteralFieldInitializer(): void { - const result = util.transpileAndExecute( - `class a { - 1: number = 4; - } - return new a()[1];` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassStringLiteralFieldInitializer") - public classStringLiteralFieldInitializer(): void { - const result = util.transpileAndExecute( - `class a { - "field": number = 4; - } - return new a()["field"];` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassComputedFieldInitializer") - public classComputedFieldInitializer(): void { - const result = util.transpileAndExecute( - `const field: "field" = "field"; - class a { - [field]: number = 4; - } - return new a()[field];` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassConstructor") - public classConstructor(): void { - const result = util.transpileAndExecute( - `class a { - field: number = 3; - constructor(n: number) { - this.field = n * 2; - } - } - return new a(4).field;` - ); - - // Assert - Expect(result).toBe(8); - } - - @Test("ClassConstructorAssignment") - public classConstructorAssignment(): void { - const result = util.transpileAndExecute( - `class a { constructor(public field: number) {} } - return new a(4).field;` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassConstructorDefaultParameter") - public classConstructorDefaultParameter(): void { - const result = util.transpileAndExecute( - `class a { public field: number; constructor(f: number = 3) { this.field = f; } } - return new a().field;` - ); - - Expect(result).toBe(3); - } - - @Test("ClassConstructorAssignmentDefault") - public classConstructorAssignmentParameterDefault(): void { - const result = util.transpileAndExecute( - `class a { constructor(public field: number = 3) { } } - return new a().field;` - ); - - Expect(result).toBe(3); - } - - @Test("ClassNewNoBrackets") - public classNewNoBrackets(): void { - const result = util.transpileAndExecute( - `class a { - public field: number = 4; - constructor() {} - } - let inst = new a; - return inst.field;` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassStaticFields") - public classStaticFields(): void { - const result = util.transpileAndExecute( - `class a { static field: number = 4; } - return a.field;` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassStaticNumericLiteralFields") - public classStaticNumericLiteralFields(): void { - const result = util.transpileAndExecute( - `class a { static 1: number = 4; } - return a[1];` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassStaticStringLiteralFields") - public classStaticStringLiteralFields(): void { - const result = util.transpileAndExecute( - `class a { static "field": number = 4; } - return a["field"];` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassStaticComputedFields") - public classStaticComputedFields(): void { - const result = util.transpileAndExecute( - `const field: "field" = "field"; - class a { static [field]: number = 4; } - return a[field];` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("classExtends") - public classExtends(): void { - const result = util.transpileAndExecute( - `class a { field: number = 4; } - class b extends a {} - return new b().field;` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("SubclassDefaultConstructor") - public subclassDefaultConstructor(): void { - const result = util.transpileAndExecute( - `class a { - field: number; - constructor(field: number) { - this.field = field; - } - } - class b extends a {} - return new b(10).field;` - ); - - Expect(result).toBe(10); - } - - @Test("SubsubclassDefaultConstructor") - public subsubclassDefaultConstructor(): void { - const result = util.transpileAndExecute( - `class a { - field: number; - constructor(field: number) { - this.field = field; - } - } - class b extends a {} - class c extends b {} - return new c(10).field;` - ); - - Expect(result).toBe(10); - } - - @Test("SubclassConstructor") - public subclassConstructor(): void { - const result = util.transpileAndExecute( - `class a { - field: number; - constructor(field: number) { - this.field = field; - } - } - class b extends a { - constructor(field: number) { - super(field + 1); - } - } - return new b(10).field;` - ); - - Expect(result).toBe(11); - } - - @Test("classSuper") - public classSuper(): void { - const result = util.transpileAndExecute( - `class a { - public field: number = 4; - constructor(n: number) { - this.field = n; - } - } - class b extends a { - constructor() { - super(5); - } - } - return new b().field;` - ); - - // Assert - Expect(result).toBe(5); - } - - @Test("classSuperSuper") - public classSuperSuper(): void { - const result = util.transpileAndExecute( - `class a { - public field: number = 4; - constructor(n: number) { - this.field = n; - } - } - class b extends a { - constructor(n: number) { - super(n * 2); - } - } - class c extends b { - constructor() { - super(5); - } - } - return new c().field;` - ); - - // Assert - Expect(result).toBe(10); - } - - @Test("classSuperSkip") - public classSuperSkip(): void { - const result = util.transpileAndExecute( - `class a { - public field: number = 4; - constructor(n: number) { - this.field = n; - } - } - class b extends a { - } - class c extends b { - constructor() { - super(5); - } - } - return new c().field;` - ); - - // Assert - Expect(result).toBe(5); - } - - @Test("renamedClassExtends") - public renamedClassExtends(): void - { - const result = util.transpileAndExecute( - `const b = new B(); - return b.value;`, - undefined, undefined, - `namespace Classes { - export class Base { - public value: number; - constructor(){ this.value = 3; } - } - } - - const A = Classes.Base; - class B extends A { - constructor(){ super(); } - };` - ); - - // Assert - Expect(result).toBe(3); - } - - @Test("ClassMethodCall") - public classMethodCall(): void { - const result = util.transpileAndExecute( - `class a { - public method(): number { - return 4; - } - } - let inst = new a(); - return inst.method();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassNumericLiteralMethodCall") - public classNumericLiteralMethodCall(): void { - const result = util.transpileAndExecute( - `class a { - public 1(): number { - return 4; - } - } - let inst = new a(); - return inst[1]();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassStringLiteralMethodCall") - public classStringLiteralMethodCall(): void { - const result = util.transpileAndExecute( - `class a { - public "method"(): number { - return 4; - } - } - let inst = new a(); - return inst["method"]();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassComputedMethodCall") - public classComputedMethodCall(): void { - const result = util.transpileAndExecute( - `const method: "method" = "method"; - class a { - public [method](): number { - return 4; - } - } - let inst = new a(); - return inst[method]();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassToString") - public classToString(): void { - const result = util.transpileAndExecute( - `class a { - public toString(): string { - return "instance of a"; - } - } - let inst = new a(); - return inst.toString();` - ); - - // Assert - Expect(result).toBe("instance of a"); - } - @Test("HasOwnProperty true") - public hasOwnProperty1(): void { - const result = util.transpileAndExecute( - `class a { - public test(): void { - } - } - let inst = new a(); - inst["prop"] = 17; - return inst.hasOwnProperty("prop");` - ); - - // Assert - Expect(result).toBe(true); - } - @Test("HasOwnProperty false") - public hasOwnProperty2(): void { - const result = util.transpileAndExecute( - `class a { - public test(): void { - } - } - let inst = new a(); - inst["prop"] = 17; - return inst.hasOwnProperty("test");` - ); - - // Assert - Expect(result).toBe(false); - } - @Test("CastClassMethodCall") - public extraParenthesisAssignment(): void { - const result = util.transpileAndExecute( - `interface result - { - val : number; - } - class a { - public method(out: result) { - out.val += 2; - } - } - let inst:any = new a(); - let result = {val : 0}; - (inst as a).method(result); - (inst as a).method(result); - return result.val;` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassPropertyFunctionThis") - public classPropertyFunctionThis(): void { - const result = util.transpileAndExecute( - `class a { - constructor(private n: number) {} - public method: () => number = () => this.n; - } - let inst = new a(4); - return inst.method();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassInheritedMethodCall") - public classInheritedMethodCall(): void { - const result = util.transpileAndExecute( - `class a { - public method(): number { - return 4; - } - } - class b extends a {} - let inst = new b(); - return inst.method();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassDoubleInheritedMethodCall") - public classDoubleInheritedMethodCall(): void { - const result = util.transpileAndExecute( - `class a { - public method(): number { - return 4; - } - } - class b extends a {} - class c extends b {} - let inst = new c(); - return inst.method();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassInheritedMethodCall2") - public classInheritedMethodCall2(): void { - const result = util.transpileAndExecute( - `class a {} - class b extends a { - public method(): number { - return 4; - } - } - class c extends b {} - let inst = new c(); - return inst.method();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("ClassMethodOverride") - public classMethodOverride(): void { - const result = util.transpileAndExecute( - `class a { - public method(): number { - return 2; - } - } - class b extends a { - public method(): number { - return 4; - } - } - let inst = new b(); - return inst.method();` - ); - - // Assert - Expect(result).toBe(4); - } - - @Test("methodDefaultParameters") - public methodInheritedParameters(): void { - const result = util.transpileAndExecute( - `class a { - public method(b: number, c: number = 5): number { - return b + c; - } - } - let inst = new a(); - return inst.method(4);` - ); - - // Assert - Expect(result).toBe(9); - } - - @Test("Class without name error") - public classWithoutNameError(): void { - const transformer = util.makeTestTransformer(); - - Expect(() => transformer.transformClassDeclaration({} as ts.ClassDeclaration)) - .toThrowError(Error, "Class declarations must have a name."); - } - - @Test("CallSuperMethodNoArgs") - public callSuperMethodNoArgs(): void { - const result = util.transpileAndExecute( - `class a { - a: number - constructor(n: number) { - this.a = n; - } - public method() { - return this.a; - } - } - class b extends a { - constructor(n: number) { - super(n); - } - public method() { - return super.method(); - } - } - let inst = new b(6); - return inst.method();` - ); - - // Assert - Expect(result).toBe(6); - } - - @Test("CallSuperMethodArgs") - public callSuperMethodArgs(): void { - const result = util.transpileAndExecute( - `class a { - a: number - constructor(n: number) { - this.a = n; - } - public method(n: number) { - return this.a + n; - } - } - class b extends a { - constructor(n: number) { - super(n); - } - public method(n: number) { - return super.method(n); - } - } - let inst = new b(6); - return inst.method(4);` - ); - - // Assert - Expect(result).toBe(10); - } - - @Test("CallSuperExpressionMethod") - public callSuperExpressionMethod(): void { - const result = util.transpileAndExecute( - `let i = 0; - function make() { - const j = i++; - return class { - constructor() {} - method() {} - }; - } - class B extends make() { - constructor() { super(); } - method() { super.method(); } - } - const inst = new B(); - inst.method(); - inst.method(); - inst.method(); - return i;` - ); - - // Assert - Expect(result).toBe(1); - } - - @Test("CallSuperSuperMethod") - public callSuperSuperMethod(): void { - const result = util.transpileAndExecute( - `class a { - a: number - constructor(n: number) { - this.a = n; - } - public method() { - return this.a; - } - } - class b extends a { - constructor(n: number) { - super(n); - } - public method() { - return super.method(); - } - } - class c extends b { - constructor(n: number) { - super(n); - } - public method() { - return super.method(); - } - } - let inst = new c(6); - return inst.method();` - ); - - // Assert - Expect(result).toBe(6); - } - - @Test("classExpression") - public classExpression(): void { - const result = util.transpileAndExecute( - `class a { - public method() { - return "instance of a"; - } - } - const b = class extends a { - public method() { - return "instance of b"; - } - } - let inst = new b(); - return inst.method();` - ); - - // Assert - Expect(result).toBe("instance of b"); - } - - @Test("classExpressionBaseClassMethod") - public classExpressionBaseClassMethod(): void { - const result = util.transpileAndExecute( - `class a { - public method() { - return 42; - } - } - const b = class extends a { - } - let inst = new b(); - return inst.method();` - ); - - // Assert - Expect(result).toBe(42); - } - - @Test("Class Method Runtime Override") - public classMethodRuntimeOverride(): void { - const result = util.transpileAndExecute( - `class MyClass { - method(): number { - return 4; - } - } - - let inst = new MyClass(); - inst.method = () => { - return 8; - } - return inst.method();` - ); - - Expect(result).toBe(8); - } - - @Test("Exported class super call") - public exportedClassSuperCall(): void { - const code = - `export class Foo { - prop: string; - constructor(prop: string) { this.prop = prop; } - } - export class Bar extends Foo { - constructor() { - super("bar"); - } - } - export const baz = (new Bar()).prop;`; - Expect(util.transpileExecuteAndReturnExport(code, "baz")).toBe("bar"); - } - - @TestCase("(new Foo())", "foo") - @TestCase("Foo", "bar") - @Test("Class method name collision") - public classMethodNameCollisiom(input: string, expectResult: string): void { - const code = - `class Foo { - public method() { return "foo"; } - public static method() { return "bar"; } - } - return ${input}.method();`; - Expect(util.transpileAndExecute(code)).toBe(expectResult); - } - - @TestCase("extension") - @TestCase("metaExtension") - @Test("Class extends extension") - public classExtendsExtension(extensionType: string): void { - const code = - `declare class A {} - /** @${extensionType} **/ - class B extends A {} - class C extends B {}`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Cannot extend classes with decorator '@extension' or '@metaExtension'." - ); - } - - @TestCase("extension") - @TestCase("metaExtension") - @Test("Class construct extension") - public classConstructExtension(extensionType: string): void { - const code = - `declare class A {} - /** @${extensionType} **/ - class B extends A {} - const b = new B();`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Cannot construct classes with decorator '@extension' or '@metaExtension'." - ); - } -} diff --git a/test/unit/classes/__snapshots__/classes.spec.ts.snap b/test/unit/classes/__snapshots__/classes.spec.ts.snap new file mode 100644 index 000000000..9154d1911 --- /dev/null +++ b/test/unit/classes/__snapshots__/classes.spec.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`missing declaration name: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +____class_0 = __TS__Class() +____class_0.name = "" +function ____class_0.prototype.____constructor(self) +end" +`; + +exports[`missing declaration name: diagnostics 1`] = `"main.ts(2,9): error TS1211: A class declaration without the 'default' modifier must have a name."`; + +exports[`super without class: code 1`] = ` +"local ____exports = {} +____exports.__result = ____.____constructor(self) +return ____exports" +`; + +exports[`super without class: diagnostics 1`] = `"main.ts(1,25): error TS2337: Super calls are not permitted outside constructors or in nested functions inside constructors."`; diff --git a/test/unit/classes/__snapshots__/decorators.spec.ts.snap b/test/unit/classes/__snapshots__/decorators.spec.ts.snap new file mode 100644 index 000000000..681e4fa3d --- /dev/null +++ b/test/unit/classes/__snapshots__/decorators.spec.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Throws error if decorator function has void context: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local __TS__Decorate = ____lualib.__TS__Decorate +local ____exports = {} +function ____exports.__main(self) + local function decorator(constructor, context) + end + local TestClass = __TS__Class() + TestClass.name = "TestClass" + function TestClass.prototype.____constructor(self) + end + TestClass = __TS__Decorate(TestClass, TestClass, {decorator}, {kind = "class", name = "TestClass"}) +end +return ____exports" +`; + +exports[`Throws error if decorator function has void context: diagnostics 1`] = `"main.ts(4,9): error TSTL: Decorator function cannot have 'this: void'."`; + +exports[`class field decorator warns the return value is ignored: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local __TS__Decorate = ____lualib.__TS__Decorate +local ____exports = {} +function ____exports.__main(self) + local fieldDecoratorContext + local function fieldDecorator(self, _, context) + fieldDecoratorContext = context + return function(____, initialValue) return initialValue * 12 end + end + local TestClass = __TS__Class() + TestClass.name = "TestClass" + function TestClass.prototype.____constructor(self) + self.value = 22 + end + __TS__Decorate(TestClass, nil, {fieldDecorator}, {kind = "field", name = "value", private = false, static = false}) +end +return ____exports" +`; + +exports[`class field decorator warns the return value is ignored: diagnostics 1`] = `"main.ts(11,13): warning TSTL: You are using a class field decorator, note that tstl ignores returned value initializers!"`; + +exports[`legacy experimentalDecorators Throws error if decorator function has void context: code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Class = ____lualib.__TS__Class +local __TS__DecorateLegacy = ____lualib.__TS__DecorateLegacy +local ____exports = {} +function ____exports.__main(self) + local function decorator(constructor) + end + local TestClass = __TS__Class() + TestClass.name = "TestClass" + function TestClass.prototype.____constructor(self) + end + TestClass = __TS__DecorateLegacy({decorator}, TestClass) +end +return ____exports" +`; + +exports[`legacy experimentalDecorators Throws error if decorator function has void context: diagnostics 1`] = `"main.ts(4,13): error TSTL: Decorator function cannot have 'this: void'."`; diff --git a/test/unit/classes/accessors.spec.ts b/test/unit/classes/accessors.spec.ts new file mode 100644 index 000000000..db17fe67d --- /dev/null +++ b/test/unit/classes/accessors.spec.ts @@ -0,0 +1,403 @@ +import * as util from "../../util"; + +test("get accessor", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + } + const f = new Foo(); + return f.foo; + `.expectToMatchJsResult(); +}); + +test("multiple get accessors", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + _bar = "bar"; + get bar() { return this._bar; } + } + const f = new Foo(); + return f.foo + f.bar; + `.expectToMatchJsResult(); +}); + +test("get accessor in base class", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + } + class Bar extends Foo {} + const b = new Bar(); + return b.foo; + `.expectToMatchJsResult(); +}); + +test("get accessor override accessor", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + } + class Bar extends Foo { + _bar = "bar"; + get foo() { return this._bar; } + } + const b = new Bar(); + return b.foo; + `.expectToMatchJsResult(); +}); + +test("get accessor override accessor (multiple)", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + } + class Bar extends Foo { + _bar = "bar"; + get foo() { return this._bar; } + } + class Baz extends Foo { + _baz = "baz"; + get foo() { return this._baz; } + } + const bar = new Bar(); + const baz = new Baz(); + return bar.foo + baz.foo; + `.expectToMatchJsResult(); +}); + +test("get accessor from interface", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + } + interface Bar { + readonly foo: string; + } + const b: Bar = new Foo(); + return b.foo; + `.expectToMatchJsResult(); +}); + +test("set accessor", () => { + util.testFunction` + class Foo { + _foo = "foo"; + set foo(val: string) { this._foo = val; } + } + const f = new Foo(); + f.foo = "bar" + return f._foo; + `.expectToMatchJsResult(); +}); + +test("set accessor in base class", () => { + util.testFunction` + class Foo { + _foo = "foo"; + set foo(val: string) { this._foo = val; } + } + class Bar extends Foo {} + const b = new Bar(); + b.foo = "bar" + return b._foo; + `.expectToMatchJsResult(); +}); + +test("set accessor override accessor", () => { + util.testFunction` + class Foo { + _foo = "foo"; + set foo(val: string) { this._foo = "foo"; } + } + class Bar extends Foo { + set foo(val: string) { this._foo = val; } + } + const b = new Bar(); + b.foo = "bar" + return b._foo; + `.expectToMatchJsResult(); +}); + +test("set accessor from interface", () => { + util.testFunction` + class Foo { + _foo = "foo"; + set foo(val: string) { this._foo = val; } + } + interface Bar { + _foo: string; + foo: string; + } + const b: Bar = new Foo(); + b.foo = "bar" + return b._foo; + `.expectToMatchJsResult(); +}); + +test("get/set accessors", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + set foo(val: string) { this._foo = val; } + } + const f = new Foo(); + const fooOriginal = f.foo; + f.foo = "bar"; + return fooOriginal + f.foo; + `.expectToMatchJsResult(); +}); + +test("multiple get/set accessors", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + set foo(val: string) { this._foo = val; } + + _bar = "bar"; + get bar() { return this._bar; } + set bar(val: string) { this._bar = val; } + } + const f = new Foo(); + const fooOriginal = f.foo; + f.foo = "fizz"; + const barOriginal = f.bar; + f.bar = "buzz"; + return [fooOriginal, f.foo, barOriginal, f.bar]; + `.expectToMatchJsResult(); +}); + +test("get/set accessors in base class", () => { + util.testFunction` + class Foo { + _foo = "foo"; + get foo() { return this._foo; } + set foo(val: string) { this._foo = val; } + } + class Bar extends Foo {} + const b = new Bar(); + const fooOriginal = b.foo; + b.foo = "bar" + return fooOriginal + b.foo; + `.expectToMatchJsResult(); +}); + +test("static get accessor", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static get foo() { return this._foo; } + } + return Foo.foo; + `.expectToMatchJsResult(); +}); + +test("static get accessor in base class", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static get foo() { return this._foo; } + } + class Bar extends Foo {} + return Bar.foo; + `.expectToMatchJsResult(); +}); + +test("static get accessor override", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static foo = "foo"; + } + class Bar extends Foo { + static get foo() { return this._foo + "bar"; } + } + return Bar.foo; + `.expectToMatchJsResult(); +}); + +test("static get accessor override accessor", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static get foo() { return this._foo; } + } + class Bar extends Foo { + static _bar = "bar"; + static get foo() { return this._bar; } + } + return Bar.foo; + `.expectToMatchJsResult(); +}); + +test("static get accessor from interface", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static get foo() { return this._foo; } + } + interface Bar { + readonly foo: string; + } + const b: Bar = Foo; + return b.foo; + `.expectToMatchJsResult(); +}); + +test("static set accessor", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static set foo(val: string) { this._foo = val; } + } + Foo.foo = "bar" + return Foo._foo; + `.expectToMatchJsResult(); +}); + +test("static set accessor in base class", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static set foo(val: string) { this._foo = val; } + } + class Bar extends Foo {} + Bar.foo = "bar" + return Bar._foo; + `.expectToMatchJsResult(); +}); + +test("static set accessor override", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static foo = "foo"; + } + class Bar extends Foo { + static set foo(val: string) { this._foo = val; } + } + Bar.foo = "bar" + return Bar._foo; + `.expectToMatchJsResult(); +}); + +test("static set accessor overridden", () => { + util.testFunction` + class Foo { + static _foo = "baz"; + static set foo(val: string) { this._foo = val; } + } + class Bar extends Foo { + static foo = "foo"; // triggers base class setter + } + const fooOriginal = Bar._foo; + Bar.foo = "bar" + return fooOriginal + Bar._foo; + `.expectToMatchJsResult(); +}); + +test("static set accessor override accessor", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static set foo(val: string) { this._foo = "foo"; } + } + class Bar extends Foo { + static set foo(val: string) { this._foo = val; } + } + Bar.foo = "bar" + return Bar._foo; + `.expectToMatchJsResult(); +}); + +test("static set accessor from interface", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static set foo(val: string) { this._foo = val; } + } + interface Bar { + _foo: string; + foo: string; + } + const b: Bar = Foo; + b.foo = "bar" + return b._foo; + `.expectToMatchJsResult(); +}); + +test("static get/set accessors", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static get foo() { return this._foo; } + static set foo(val: string) { this._foo = val; } + } + const fooOriginal = Foo.foo; + Foo.foo = "bar"; + return fooOriginal + Foo.foo; + `.expectToMatchJsResult(); +}); + +test("static get/set accessors in base class", () => { + util.testFunction` + class Foo { + static _foo = "foo"; + static get foo() { return this._foo; } + static set foo(val: string) { this._foo = val; } + } + class Bar extends Foo {} + const fooOriginal = Bar.foo; + Bar.foo = "bar" + return fooOriginal + Bar.foo; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1437 +test("super class get accessor (#1437)", () => { + util.testFunction` + class A { + get foo() { + return "A"; + } + } + + class B extends A { + override get foo() { + return super.foo + "B"; + } + } + + return new B().foo; + `.expectToMatchJsResult(); +}); + +test("super class set accessor", () => { + util.testFunction` + let result = "unset"; + + class A { + set result(value: string) { + result = "foo" + value; + } + } + + class B extends A { + override set result(value: string) { + super.result = "bar" + value; + } + } + + new B().result = "baz"; + + return result; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/classes/classes.spec.ts b/test/unit/classes/classes.spec.ts new file mode 100644 index 000000000..f28c3b43d --- /dev/null +++ b/test/unit/classes/classes.spec.ts @@ -0,0 +1,923 @@ +import * as util from "../../util"; + +test("ClassFieldInitializer", () => { + util.testFunction` + class a { + field: number = 4; + } + return new a().field; + `.expectToMatchJsResult(); +}); + +test("ClassNumericLiteralFieldInitializer", () => { + util.testFunction` + class a { + 1: number = 4; + } + return new a()[1]; + `.expectToMatchJsResult(); +}); + +test("ClassStringLiteralFieldInitializer", () => { + util.testFunction` + class a { + "field": number = 4; + } + return new a()["field"]; + `.expectToMatchJsResult(); +}); + +test("ClassComputedFieldInitializer", () => { + util.testFunction` + const field: "field" = "field"; + class a { + [field]: number = 4; + } + return new a()[field]; + `.expectToMatchJsResult(); +}); + +test("ClassConstructor", () => { + util.testFunction` + class a { + field: number = 3; + constructor(n: number) { + this.field = n * 2; + } + } + return new a(4).field; + `.expectToMatchJsResult(); +}); + +test("ClassConstructorAssignment", () => { + util.testFunction` + class a { constructor(public field: number) {} } + return new a(4).field; + `.expectToMatchJsResult(); +}); + +test("ClassConstructorDefaultParameter", () => { + util.testFunction` + class a { public field: number; constructor(f: number = 3) { this.field = f; } } + return new a().field; + `.expectToMatchJsResult(); +}); + +test("ClassConstructorAssignmentDefault", () => { + util.testFunction` + class a { constructor(public field: number = 3) { } } + return new a().field; + `.expectToMatchJsResult(); +}); + +test("ClassConstructorPropertyInitiailizationOrder", () => { + util.testFunction` + class Test { + public foo = this.bar; + constructor(public bar: string) {} + } + return (new Test("baz")).foo; + `.expectToMatchJsResult(); +}); + +test("ClassConstructorPropertyInitiailizationFalsey", () => { + util.testFunction` + class Test { + constructor(public foo = true) {} + } + return (new Test(false)).foo; + `.expectToMatchJsResult(); +}); + +test("ClassNewNoBrackets", () => { + util.testFunction` + class a { + public field: number = 4; + constructor() {} + } + let inst = new a; + return inst.field; + `.expectToMatchJsResult(); +}); + +test("ClassStaticFields", () => { + util.testFunction` + class a { static field: number = 4; } + return a.field; + `.expectToMatchJsResult(); +}); + +test("ClassStaticNumericLiteralFields", () => { + util.testFunction` + class a { static 1: number = 4; } + return a[1]; + `.expectToMatchJsResult(); +}); + +test("ClassStaticStringLiteralFields", () => { + util.testFunction` + class a { static "field": number = 4; } + return a["field"]; + `.expectToMatchJsResult(); +}); + +test("ClassStaticComputedFields", () => { + util.testFunction` + const field: "field" = "field"; + class a { static [field]: number = 4; } + return a[field]; + `.expectToMatchJsResult(); +}); + +test("classExtends", () => { + util.testFunction` + class a { field: number = 4; } + class b extends a {} + return new b().field; + `.expectToMatchJsResult(); +}); + +test("SubclassDefaultConstructor", () => { + util.testFunction` + class a { + field: number; + constructor(field: number) { + this.field = field; + } + } + class b extends a {} + return new b(10).field; + `.expectToMatchJsResult(); +}); + +test("SubsubclassDefaultConstructor", () => { + util.testFunction` + class a { + field: number; + constructor(field: number) { + this.field = field; + } + } + class b extends a {} + class c extends b {} + return new c(10).field; + `.expectToMatchJsResult(); +}); + +test("SubclassConstructor", () => { + util.testFunction` + class a { + field: number; + constructor(field: number) { + this.field = field; + } + } + class b extends a { + constructor(field: number) { + super(field + 1); + } + } + return new b(10).field; + `.expectToMatchJsResult(); +}); + +test("SubclassConstructorPropertyInitiailizationSuperOrder", () => { + util.testFunction` + class a { + field: number; + constructor(field: number) { + this.field = field; + } + } + class b extends a { + fieldDouble = this.field * 2; + constructor(field: number) { + const newField = field + 1; + super(newField); + } + } + + const result = new b(10); + return [result.field, result.fieldDouble]; + `.expectToMatchJsResult(); +}); + +test("Subclass constructor across merged namespace", () => { + util.testModule` + namespace NS { + export class Super { + prop: string; + constructor() { + this.prop = "foo"; + } + } + } + namespace NS { + export class Sub extends Super { + constructor() { + super(); + } + } + } + export const result = (new NS.Sub()).prop; + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("super without class", () => { + util.testExpression`super()`.expectDiagnosticsToMatchSnapshot([2337]); +}); + +test("super in unnamed class", () => { + util.testFunction` + class Foo { + public x = true; + } + + const Bar = (class extends (Foo) { + constructor() { + super(); + } + }); + + return new Bar().x; + `.expectToMatchJsResult(); +}); + +test("classSuper", () => { + util.testFunction` + class a { + public field: number = 4; + constructor(n: number) { + this.field = n; + } + } + class b extends a { + constructor() { + super(5); + } + } + return new b().field; + `.expectToMatchJsResult(); +}); + +test("classSuperSuper", () => { + util.testFunction` + class a { + public field: number = 4; + constructor(n: number) { + this.field = n; + } + } + class b extends a { + constructor(n: number) { + super(n * 2); + } + } + class c extends b { + constructor() { + super(5); + } + } + return new c().field; + `.expectToMatchJsResult(); +}); + +test("classSuperSkip", () => { + util.testFunction` + class a { + public field: number = 4; + constructor(n: number) { + this.field = n; + } + } + class b extends a { + } + class c extends b { + constructor() { + super(5); + } + } + return new c().field; + `.expectToMatchJsResult(); +}); + +test("renamedClassExtends", () => { + util.testModule` + namespace Classes { + export class Base { + public value: number; + constructor(){ this.value = 3; } + } + } + + const A = Classes.Base; + class B extends A { + constructor(){ super(); } + } + + const b = new B(); + export const result = b.value; + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("ClassMethodCall", () => { + util.testFunction` + class a { + public method(): number { + return 4; + } + } + let inst = new a(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("ClassNumericLiteralMethodCall", () => { + util.testFunction` + class a { + public 1(): number { + return 4; + } + } + let inst = new a(); + return inst[1](); + `.expectToMatchJsResult(); +}); + +test("ClassStringLiteralMethodCall", () => { + util.testFunction` + class a { + public "method"(): number { + return 4; + } + } + let inst = new a(); + return inst["method"](); + `.expectToMatchJsResult(); +}); + +test("ClassComputedMethodCall", () => { + util.testFunction` + const method: "method" = "method"; + class a { + public [method](): number { + return 4; + } + } + let inst = new a(); + return inst[method](); + `.expectToMatchJsResult(); +}); + +test("CastClassMethodCall", () => { + util.testFunction` + interface result + { + val : number; + } + class a { + public method(out: result) { + out.val += 2; + } + } + let inst:any = new a(); + let result = {val : 0}; + (inst as a).method(result); + (inst as a).method(result); + return result.val; + `.expectToMatchJsResult(); +}); + +test("ClassPropertyFunctionThis", () => { + util.testFunction` + class a { + constructor(private n: number) {} + public method: () => number = () => this.n; + } + let inst = new a(4); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("ClassInheritedMethodCall", () => { + util.testFunction` + class a { + public method(): number { + return 4; + } + } + class b extends a {} + let inst = new b(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("ClassDoubleInheritedMethodCall", () => { + util.testFunction` + class a { + public method(): number { + return 4; + } + } + class b extends a {} + class c extends b {} + let inst = new c(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("ClassInheritedMethodCall2", () => { + util.testFunction` + class a {} + class b extends a { + public method(): number { + return 4; + } + } + class c extends b {} + let inst = new c(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("ClassMethodOverride", () => { + util.testFunction` + class a { + public method(): number { + return 2; + } + } + class b extends a { + public method(): number { + return 4; + } + } + let inst = new b(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("methodDefaultParameters", () => { + util.testFunction` + class a { + public method(b: number, c: number = 5): number { + return b + c; + } + } + let inst = new a(); + return inst.method(4); + `.expectToMatchJsResult(); +}); + +test("CallSuperMethodNoArgs", () => { + util.testFunction` + class a { + a: number + constructor(n: number) { + this.a = n; + } + public method() { + return this.a; + } + } + class b extends a { + constructor(n: number) { + super(n); + } + public method() { + return super.method(); + } + } + let inst = new b(6); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("CallSuperMethodArgs", () => { + util.testFunction` + class a { + a: number + constructor(n: number) { + this.a = n; + } + public method(n: number) { + return this.a + n; + } + } + class b extends a { + constructor(n: number) { + super(n); + } + public method(n: number) { + return super.method(n); + } + } + let inst = new b(6); + return inst.method(4); + `.expectToMatchJsResult(); +}); + +test("CallSuperExpressionMethod", () => { + util.testFunction` + let i = 0; + function make() { + const j = i++; + return class { + constructor() {} + method() {} + }; + } + class B extends make() { + constructor() { super(); } + method() { super.method(); } + } + const inst = new B(); + inst.method(); + inst.method(); + inst.method(); + return i; + `.expectToMatchJsResult(); +}); + +test("CallSuperSuperMethod", () => { + util.testFunction` + class a { + a: number + constructor(n: number) { + this.a = n; + } + public method() { + return this.a; + } + } + class b extends a { + constructor(n: number) { + super(n); + } + public method() { + return super.method(); + } + } + class c extends b { + constructor(n: number) { + super(n); + } + public method() { + return super.method(); + } + } + let inst = new c(6); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("classExpression", () => { + util.testFunction` + class a { + public method() { + return "instance of a"; + } + } + const b = class extends a { + public method() { + return "instance of b"; + } + } + let inst = new b(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("Named Class Expression", () => { + util.testFunction` + const a = class MyClass { + public method() { + return "foo"; + } + } + let inst = new a(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("classExpressionBaseClassMethod", () => { + util.testFunction` + class a { + public method() { + return 42; + } + } + const b = class extends a { + } + let inst = new b(); + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("Class Method Runtime Override", () => { + util.testFunction` + class MyClass { + method(): number { + return 4; + } + } + + let inst = new MyClass(); + inst.method = () => { + return 8; + } + return inst.method(); + `.expectToMatchJsResult(); +}); + +test("Exported class super call", () => { + util.testModule` + export class Foo { + prop: string; + constructor(prop: string) { this.prop = prop; } + } + export class Bar extends Foo { + constructor() { + super("bar"); + } + } + export const result = (new Bar()).prop; + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test.each(["(new Foo())", "Foo"])("Class method name collision (%p)", input => { + util.testFunction` + class Foo { + public method() { return "foo"; } + public static method() { return "bar"; } + } + return ${input}.method(); + `.expectToMatchJsResult(); +}); + +test("Class static instance of self", () => { + util.testFunction` + class Foo { + bar = "foobar"; + static instance = new Foo(); + } + return Foo.instance.bar; + `.expectToMatchJsResult(); +}); + +test("Class name", () => { + util.testFunction` + class Foo {} + return Foo.name; + `.expectToMatchJsResult(); +}); + +test("Class name via constructor", () => { + util.testFunction` + class Foo {} + const foo = new Foo(); + return foo.constructor.name; + `.expectToMatchJsResult(); +}); + +test("Class expression name", () => { + util.testFunction` + const foo = class Foo {}; + return foo.name; + `.expectToMatchJsResult(); +}); + +test("Class expression name via constructor", () => { + util.testFunction` + const foo = class Foo {}; + const bar = new foo(); + return bar.constructor.name; + `.expectToMatchJsResult(); +}); + +test("Anonymous class in variable declaration has name", () => { + util.testFunction` + const foo = class {}; + const bar = foo; + return { a: foo.name, b: bar.name }; + `.expectToMatchJsResult(); +}); + +test("Anonymous class expression outside variable assignment", () => { + util.testExpression`(class {}).name`.expectToMatchJsResult(); +}); + +test("Class anonymous expression name via constructor", () => { + util.testFunction` + const foo = class {}; + const bar = new foo(); + return bar.constructor.name; + `.expectToMatchJsResult(); +}); + +test("Class field override in subclass", () => { + util.testFunction` + class Foo { + field = "foo"; + } + class Bar extends Foo { + field = "bar"; + } + return (new Foo()).field + (new Bar()).field; + `.expectToMatchJsResult(); +}); + +test("Class field override in subclass with constructors", () => { + util.testFunction` + class Foo { + field = "foo"; + constructor() {} + } + class Bar extends Foo { + field = "bar"; + constructor() { super(); } + } + return (new Foo()).field + (new Bar()).field; + `.expectToMatchJsResult(); +}); + +test("missing declaration name", () => { + util.testModule` + class {} + `.expectDiagnosticsToMatchSnapshot([1211]); +}); + +test("default exported name class has correct name property", () => { + util.testModule` + export default class Test { static method() { return true; } } + ` + .setReturnExport("default", "name") + .expectToMatchJsResult(); +}); + +test("default exported anonymous class has 'default' name property", () => { + util.testModule` + export default class { static method() { return true; } } + ` + .setReturnExport("default", "name") + .expectToEqual("default"); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/584 +test("constructor class name available with decorator", () => { + util.testModule` + const decorator = any>(constructor: T, context: ClassDecoratorContext) => class extends constructor {}; + + @decorator + class MyClass {} + + export const className = new MyClass().constructor.name; + ` + .setReturnExport("className") + .expectToEqual("MyClass"); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/959 +test("methods accessed via this index pass correct context", () => { + util.testModule` + class Example { + baz = 3; + foo() { + this["bar"]() + } + + bar() { + return this.baz; + } + } + + const inst = new Example(); + export const result = inst.foo(); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/959 +test.each(['(this["bar"])', '((((this["bar"]))))'])("methods in parentheses pass correct context %s", callPath => { + util.testModule` + class Example { + baz = 3; + foo() { + ${callPath}() + } + + bar() { + return this.baz; + } + } + + const inst = new Example(); + export const result = inst.foo(); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1447 +test("static initialization block (#1447)", () => { + util.testModule` + class A { + private static staticProperty1 = 3; + public static staticProperty2; + static { + this.staticProperty2 = this.staticProperty1 + 5; + } + public static staticProperty3; + static { + this.staticProperty3 = this.staticProperty1 + this.staticProperty2; + } + } + export const result1 = A.staticProperty2; + export const result2 = A.staticProperty3; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1457 +test("static member definition order (#1457)", () => { + util.testFunction` + class A { + static a = A.foo() + + static foo(): number { + return 5 + } + } + + return A.a; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1504 +test("Calling static inherited functions works (#1504)", () => { + util.testFunction` + class A { + static Get() { + return "A"; + } + } + + class B extends A { + static Get() { + return super.Get() + "B"; + } + } + + return B.Get(); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1537 +test("get inherted __index member from super (DotA 2 inheritance) (#1537)", () => { + util.testFunction` + // Inherit 'connected' class + class C extends Connected { + bar() { + return super.foo(); + } + } + + return new C().bar();` + .setTsHeader( + `interface I { + foo(): string; + } + + // Hacky interface/class merging + interface Connected extends I {} + class Connected {} + + declare function setmetatable(this: void, t: any, mt: any); + + const A = { + foo() { + return "foo"; + } + }; + + // Connect class 'Connected' to 'traditional' class A + setmetatable(Connected.prototype, { + __index: A + });` + ) + .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 new file mode 100644 index 000000000..4782746be --- /dev/null +++ b/test/unit/classes/decorators.spec.ts @@ -0,0 +1,648 @@ +import { + decoratorInvalidContext, + incompleteFieldDecoratorWarning, +} from "../../../src/transformation/utils/diagnostics"; +import * as util from "../../util"; + +test("Class decorator with no parameters", () => { + util.testFunction` + let classDecoratorContext; + + function classDecorator {}>(constructor: T, context: ClassDecoratorContext) { + classDecoratorContext = context; + + return class extends constructor { + decoratorBool = true; + } + } + + @classDecorator + class TestClass { + public decoratorBool = false; + } + + return { decoratedClass: new TestClass(), context: { + kind: classDecoratorContext.kind, + name: classDecoratorContext.name, + } }; + `.expectToMatchJsResult(); +}); + +test("Class decorator with parameters", () => { + util.testFunction` + function setNum(numArg: number) { + return {}>(constructor: T, context: ClassDecoratorContext) => { + return class extends constructor { + decoratorNum = numArg; + }; + }; + } + + @setNum(420) + class TestClass { + public decoratorNum; + } + + return new TestClass(); + `.expectToMatchJsResult(); +}); + +test("Multiple class decorators", () => { + util.testFunction` + function setTen {}>(constructor: T, context: ClassDecoratorContext) { + return class extends constructor { + decoratorTen = 10; + } + } + + function setNum {}>(constructor: T, context: ClassDecoratorContext) { + return class extends constructor { + decoratorNum = 410; + } + } + + @setTen + @setNum + class TestClass { + public decoratorTen; + public decoratorNum; + } + + return new TestClass(); + `.expectToMatchJsResult(); +}); + +test("Class decorator with inheritance", () => { + util.testFunction` + function setTen {}>(constructor: T, context: ClassDecoratorContext) { + return class extends constructor { + decoratorTen = 10; + } + } + + class TestClass { + public decoratorTen = 0; + } + + @setTen + class SubTestClass extends TestClass { + public decoratorTen = 5; + } + + return new SubTestClass(); + `.expectToMatchJsResult(); +}); + +test("Class decorators are applied in order and executed in reverse order", () => { + util.testFunction` + const order = []; + + function pushOrder(index: number) { + order.push("eval " + index); + return (constructor: new (...args: any[]) => {}, context: ClassDecoratorContext) => { + order.push("execute " + index); + }; + } + + @pushOrder(1) + @pushOrder(2) + @pushOrder(3) + class TestClass {} + + return order; + `.expectToMatchJsResult(); +}); + +test("Throws error if decorator function has void context", () => { + util.testFunction` + function decorator(this: void, constructor: new (...args: any[]) => {}, context: ClassDecoratorContext) {} + + @decorator + class TestClass {} + `.expectDiagnosticsToMatchSnapshot([decoratorInvalidContext.code]); +}); + +test("Exported class decorator", () => { + util.testModule` + function decorator any>(Class: T, context: ClassDecoratorContext): T { + return class extends Class { + public bar = "foobar"; + }; + } + + @decorator + export class Foo {} + ` + .setReturnExport("Foo", "bar") + .expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1149 +test("exported class with decorator (#1149)", () => { + util.testModule` + import { MyClass } from "./other"; + const inst = new MyClass(); + export const result = inst.foo(); + ` + .addExtraFile( + "other.ts", + `function myDecorator(target: {new(): any}, context: ClassDecoratorContext) { + return class extends target { + foo() { + return "overridden"; + } + } + } + + @myDecorator + export class MyClass { + foo() { + return "foo"; + } + }` + ) + .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"; + const inst = new MyClass(); + export const result = inst.foo(); + ` + .addExtraFile( + "other.ts", + `function myDecorator(target: {new(): any}, context: ClassDecoratorContext) { + return class extends target { + foo() { + return "overridden"; + } + } + } + + @myDecorator + export default class { + foo() { + return "foo"; + } + }` + ) + .expectToEqual({ result: "overridden" }); +}); + +test("class method decorator", () => { + util.testFunction` + let methodDecoratorContext; + + function methodDecorator(method: (v: number) => number, context: ClassMethodDecoratorContext) { + methodDecoratorContext = context; + + return (v: number) => { + return method(v) + 10; + }; + } + + class TestClass { + @methodDecorator + public myMethod(x: number) { + return x * 23; + } + } + + return { result: new TestClass().myMethod(4), context: { + kind: methodDecoratorContext.kind, + name: methodDecoratorContext.name, + private: methodDecoratorContext.private, + static: methodDecoratorContext.static + } }; + `.expectToMatchJsResult(); +}); + +test("this in decorator points to class being decorated", () => { + util.testFunction` + function methodDecorator(method: (v: number) => number, context: ClassMethodDecoratorContext) { + return function() { + const thisCallTime = this.myInstanceVariable; + return thisCallTime; + }; + } + + class TestClass { + constructor(protected myInstanceVariable: number) { } + + @methodDecorator + public myMethod() { + return 0; + } + } + + return new TestClass(5).myMethod(); + `.expectToMatchJsResult(); +}); + +test("class getter decorator", () => { + util.testFunction` + let getterDecoratorContext; + + function getterDecorator(getter: () => number, context: ClassGetterDecoratorContext) { + getterDecoratorContext = context; + + return () => { + return getter() + 10; + }; + } + + class TestClass { + @getterDecorator + get getterValue() { return 10; } + } + + return { result: new TestClass().getterValue, context: { + kind: getterDecoratorContext.kind, + name: getterDecoratorContext.name, + private: getterDecoratorContext.private, + static: getterDecoratorContext.static + } }; + `.expectToMatchJsResult(); +}); + +test("class setter decorator", () => { + util.testFunction` + let setterDecoratorContext; + + function setterDecorator(setter: (v: number) => void, context: ClassSetterDecoratorContext) { + setterDecoratorContext = context; + + return function(v: number) { + setter.call(this, v + 15); + }; + } + + class TestClass { + public value: number; + + @setterDecorator + set valueSetter(v: number) { this.value = v; } + } + + const instance = new TestClass(); + instance.valueSetter = 23; + return { result: instance.value, context: { + kind: setterDecoratorContext.kind, + name: setterDecoratorContext.name, + private: setterDecoratorContext.private, + static: setterDecoratorContext.static + } }; + `.expectToMatchJsResult(); +}); + +test("class field decorator", () => { + util.testFunction` + let fieldDecoratorContext; + + function fieldDecorator(_: undefined, context: ClassFieldDecoratorContext) { + fieldDecoratorContext = context; + } + + class TestClass { + @fieldDecorator + public value: number = 22; + } + + return { result: new TestClass(), context: { + kind: fieldDecoratorContext.kind, + name: fieldDecoratorContext.name, + private: fieldDecoratorContext.private, + static: fieldDecoratorContext.static, + } }; + `.expectToEqual({ + result: { + value: 22, // Different from JS because we ignore the value initializer + }, + context: { + kind: "field", + name: "value", + private: false, + static: false, + }, + }); +}); + +test("class field decorator warns the return value is ignored", () => { + util.testFunction` + let fieldDecoratorContext; + + function fieldDecorator(_: undefined, context: ClassFieldDecoratorContext) { + fieldDecoratorContext = context; + + return (initialValue: number) => initialValue * 12; + } + + class TestClass { + @fieldDecorator + public value: number = 22; + } + `.expectDiagnosticsToMatchSnapshot([incompleteFieldDecoratorWarning.code]); +}); + +describe("legacy experimentalDecorators", () => { + test("Class decorator with no parameters", () => { + util.testFunction` + function setBool {}>(constructor: T) { + return class extends constructor { + decoratorBool = true; + } + } + + @setBool + class TestClass { + public decoratorBool = false; + } + + return new TestClass(); + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test("Class decorator with parameters", () => { + util.testFunction` + function setNum(numArg: number) { + return {}>(constructor: T) => { + return class extends constructor { + decoratorNum = numArg; + }; + }; + } + + @setNum(420) + class TestClass { + public decoratorNum; + } + + return new TestClass(); + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test("Multiple class decorators", () => { + util.testFunction` + function setTen {}>(constructor: T) { + return class extends constructor { + decoratorTen = 10; + } + } + + function setNum {}>(constructor: T) { + return class extends constructor { + decoratorNum = 410; + } + } + + @setTen + @setNum + class TestClass { + public decoratorTen; + public decoratorNum; + } + + return new TestClass(); + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test("Class decorator with inheritance", () => { + util.testFunction` + function setTen {}>(constructor: T) { + return class extends constructor { + decoratorTen = 10; + } + } + + class TestClass { + public decoratorTen = 0; + } + + @setTen + class SubTestClass extends TestClass { + public decoratorTen = 5; + } + + return new SubTestClass(); + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test("Class decorators are applied in order and executed in reverse order", () => { + util.testFunction` + const order = []; + + function pushOrder(index: number) { + order.push("eval " + index); + return (constructor: new (...args: any[]) => {}) => { + order.push("execute " + index); + }; + } + + @pushOrder(1) + @pushOrder(2) + @pushOrder(3) + class TestClass {} + + return order; + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test("Throws error if decorator function has void context", () => { + util.testFunction` + function decorator(this: void, constructor: new (...args: any[]) => {}) {} + + @decorator + class TestClass {} + ` + .setOptions({ experimentalDecorators: true }) + .expectDiagnosticsToMatchSnapshot([decoratorInvalidContext.code]); + }); + + test("Exported class decorator", () => { + util.testModule` + function decorator any>(Class: T): T { + return class extends Class { + public bar = "foobar"; + }; + } + + @decorator + export class Foo {} + ` + .setReturnExport("Foo", "bar") + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test.each([ + ["@decorator method() {}"], + ["@decorator property;"], + ["@decorator propertyWithInitializer = () => {};"], + ["@decorator ['evaluated property'];"], + ["@decorator get getter() { return 5 }"], + ["@decorator set setter(value) {}"], + ["@decorator static method() {}"], + ["@decorator static property;"], + ["@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) {}"], + ])("Decorate class member (%p)", classMember => { + util.testFunction` + let decoratorParameters: any; + + const decorator = (target, key, index?) => { + const targetKind = target === Foo ? "Foo" : target === Foo.prototype ? "Foo.prototype" : "unknown"; + decoratorParameters = { targetKind, key, index: typeof index }; + }; + + class Foo { + ${classMember} + } + + return decoratorParameters; + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + describe("Decorators /w descriptors", () => { + test.each([ + ["return { writable: true }", "return { configurable: true }"], + ["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} }; + class Foo { @A @B static method() {} } + const { value, ...rest } = Object.getOwnPropertyDescriptor(Foo, "method"); + return rest; + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); + + test.each(["return { value: true }", "desc.value = true"])( + "Use decorator to override method value %s", + overrideStatement => { + util.testFunction` + const decorator = (target, key, desc): any => { ${overrideStatement} }; + class Foo { @decorator static method() {} } + return Foo.method; + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + } + ); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1149 + test("exported class with decorator", () => { + util.testModule` + import { MyClass } from "./other"; + const inst = new MyClass(); + export const result = inst.foo(); + ` + .addExtraFile( + "other.ts", + `function myDecorator(target: {new(): any}) { + return class extends target { + foo() { + return "overridden"; + } + } + } + + @myDecorator + export class MyClass { + foo() { + return "foo"; + } + }` + ) + .setOptions({ experimentalDecorators: true }) + .expectToEqual({ result: "overridden" }); + }); + + test("default exported class with decorator", () => { + util.testModule` + import MyClass from "./other"; + const inst = new MyClass(); + export const result = inst.foo(); + ` + .addExtraFile( + "other.ts", + `function myDecorator(target: {new(): any}) { + return class extends target { + foo() { + return "overridden"; + } + } + } + + @myDecorator + export default class { + foo() { + return "foo"; + } + }` + ) + .setOptions({ experimentalDecorators: true }) + .expectToEqual({ result: "overridden" }); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1453 + test("Class methods with legacy decorators can still be called ($1453)", () => { + util.testFunction` + function decorator( + target: Class, + propertyKey: keyof Class, + ): void {} + + class Foo { + @decorator + public method2() { return 4; } + } + + return new Foo().method2(); + ` + .setOptions({ experimentalDecorators: true }) + .expectToMatchJsResult(); + }); +}); diff --git a/test/unit/classes/instanceof.spec.ts b/test/unit/classes/instanceof.spec.ts new file mode 100644 index 000000000..3869bfa1c --- /dev/null +++ b/test/unit/classes/instanceof.spec.ts @@ -0,0 +1,98 @@ +import * as util from "../../util"; + +test("instanceof", () => { + util.testFunction` + class myClass {} + const instance = new myClass(); + return instance instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("instanceof inheritance", () => { + util.testFunction` + class myClass {} + class childClass extends myClass {} + const instance = new childClass(); + return instance instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("instanceof inheritance false", () => { + util.testFunction` + class myClass {} + class childClass extends myClass {} + const instance = new myClass(); + return instance instanceof childClass; + `.expectToMatchJsResult(); +}); + +test("{} instanceof Object", () => { + util.testExpression`{} instanceof Object`.expectToMatchJsResult(); +}); + +test("function instanceof Object", () => { + util.testExpression`(() => {}) instanceof Object`.expectToMatchJsResult(); +}); + +test("null instanceof Object", () => { + util.testExpression`(null as any) instanceof Object`.expectToMatchJsResult(); +}); + +test("instanceof undefined", () => { + util.testExpression`{} instanceof (undefined as any)`.expectToMatchJsResult(true); +}); + +test("null instanceof Class", () => { + util.testFunction` + class myClass {} + return (null as any) instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("function instanceof Class", () => { + util.testFunction` + class myClass {} + const noop = () => {}; + return (noop as any) instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("boolean instanceof Class", () => { + util.testFunction` + class myClass {} + return (false as any) instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("number instanceof Class", () => { + util.testFunction` + class myClass {} + return (5 as any) instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("instanceof export", () => { + util.testModule` + export class myClass {} + const instance = new myClass(); + export const result = instance instanceof myClass; + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("instanceof Symbol.hasInstance", () => { + util.testFunction` + class myClass { + static [Symbol.hasInstance](instance: unknown) { + return false; + } + } + + const instance = new myClass(); + const isInstanceOld = instance instanceof myClass; + myClass[Symbol.hasInstance] = () => true; + const isInstanceNew = instance instanceof myClass; + return { isInstanceOld, isInstanceNew }; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/commandLineParser.spec.ts b/test/unit/commandLineParser.spec.ts deleted file mode 100644 index 3b9bceacd..000000000 --- a/test/unit/commandLineParser.spec.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import { findConfigFile, parseCommandLine } from "../../src/CommandLineParser"; -import { LuaTarget, LuaLibImportKind } from "../../src/CompilerOptions"; - -export class CommandLineParserTests -{ - @TestCase([""], LuaLibImportKind.Inline) - @TestCase(["--luaLibImport", "none"], LuaLibImportKind.None) - @TestCase(["--luaLibImport", "always"], LuaLibImportKind.Always) - @TestCase(["--luaLibImport", "inline"], LuaLibImportKind.Inline) - @TestCase(["--luaLibImport", "require"], LuaLibImportKind.Require) - @Test("CLI parser luaLibImportKind") - public cliParserLuaLibImportKind(args: string[], expected: LuaLibImportKind): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.luaLibImport).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @Test("CLI parser invalid luaLibImportKind") - public cliParserInvalidLuaLibImportKind(): void { - const result = parseCommandLine(["--luaLibImport", "invalid"]); - Expect(result.isValid).toBe(false); - } - - @TestCase([""], LuaTarget.LuaJIT) - @TestCase(["--luaTarget", "5.1"], LuaTarget.Lua51) - @TestCase(["--luaTarget", "5.2"], LuaTarget.Lua52) - @TestCase(["--luaTarget", "jit"], LuaTarget.LuaJIT) - @TestCase(["--luaTarget", "JIT"], LuaTarget.LuaJIT) - @TestCase(["--luaTarget", "5.3"], LuaTarget.Lua53) - @Test("CLI parser luaTarget") - public cliParserLuaTarget(args: string[], expected: LuaTarget): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.luaTarget).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @TestCase(["-lt", "5.1"], LuaTarget.Lua51) - @TestCase(["-lt", "5.2"], LuaTarget.Lua52) - @TestCase(["-lt", "jit"], LuaTarget.LuaJIT) - @TestCase(["-lt", "JIT"], LuaTarget.LuaJIT) - @TestCase(["-lt", "5.3"], LuaTarget.Lua53) - @Test("CLI parser luaTarget") - public cliParserLuaTargetAlias(args: string[], expected: LuaTarget): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.luaTarget).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @Test("CLI parser invalid luaTarget") - public cliParserInvalidLuaTarget(): void { - const result = parseCommandLine(["--luatTarget", "invalid"]); - Expect(result.isValid).toBe(false); - } - - @TestCase([""], false) - @TestCase(["--noHeader", "true"], true) - @TestCase(["--noHeader", "false"], false) - @Test("CLI parser noHeader") - public cliParserNoHeader(args: string[], expected: boolean): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.noHeader).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @TestCase([""], false) - @TestCase(["--project", "tsconfig.json"], true) - @TestCase(["-p", "tsconfig.json"], true) - @Test("CLI parser project") - public cliParserProject(args: string[], expected: boolean): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.project !== undefined).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @TestCase([""], false) - @TestCase(["--help"], true) - @TestCase(["-h"], true) - @Test("CLI parser project") - public cliParserHelp(args: string[], expected: boolean): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.help === true).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @TestCase([""], false) - @TestCase(["--version"], true) - @TestCase(["-v"], true) - @Test("CLI parser project") - public cliParserVersion(args: string[], expected: boolean): void { - const result = parseCommandLine(args); - if (result.isValid === true) { - Expect(result.result.options.version === true).toBe(expected); - } else { - Expect(result.isValid).toBeTruthy(); - } - } - - @Test("defaultOption") - @TestCase("luaTarget", LuaTarget.LuaJIT) - @TestCase("noHeader", false) - @TestCase("luaLibImport", "inline") - @TestCase("rootDir", process.cwd()) - @TestCase("outDir", process.cwd()) - public defaultOptions(option: any, expected: any): void { - const parsedCommandLine = parseCommandLine([]); - if (parsedCommandLine.isValid) { - Expect(expected).toBe(parsedCommandLine.result.options[option]); - } else { - Expect(parsedCommandLine.isValid).toBeTruthy(); - } - } - - @Test("ValidLuaTarget") - public validLuaTarget(): void { - const parsedCommandLine = parseCommandLine(['--luaTarget', '5.3']); - if (parsedCommandLine.isValid) { - Expect(parsedCommandLine.result.options["luaTarget"]).toBe("5.3"); - } else { - Expect(parsedCommandLine.isValid).toBeTruthy(); - } - } - - @Test("InvalidLuaTarget") - public invalidLuaTarget(): void { - // Don't check error message because the yargs library messes the message up. - const result = parseCommandLine(['--luaTarget', '42']); - Expect(result.isValid).toBe(false); - } - - @Test("InvalidArgumentTSTL") - public invalidArgument(): void { - // Don't check error message because the yargs library messes the message up. - const result = parseCommandLine(['--invalidTarget', 'test']); - Expect(result.isValid).toBe(false); - } - - @Test("outDir") - public outDir(): void { - const parsedCommandLine = parseCommandLine(['--outDir', './test']); - - if (parsedCommandLine.isValid) { - Expect(parsedCommandLine.result.options['outDir']).toBe('./test'); - } else { - Expect(parsedCommandLine.isValid).toBeTruthy(); - } - } - - @Test("rootDir") - public rootDir(): void { - const parsedCommandLine = parseCommandLine(['--rootDir', './test']); - - if (parsedCommandLine.isValid) { - Expect(parsedCommandLine.result.options['rootDir']).toBe('./test'); - Expect(parsedCommandLine.result.options['outDir']).toBe('./test'); - } else { - Expect(parsedCommandLine.isValid).toBeTruthy(); - } - } - - @Test("outDirAndRooDir") - public outDirAndRooDir(): void { - const parsedCommandLine = parseCommandLine(['--outDir', './testOut', '--rootDir', './testRoot']); - - if (parsedCommandLine.isValid) { - Expect(parsedCommandLine.result.options['outDir']).toBe('./testOut'); - Expect(parsedCommandLine.result.options['rootDir']).toBe('./testRoot'); - } else { - Expect(parsedCommandLine.isValid).toBeTruthy(); - } - } - - @Test("Find config no path") - public findConfigNoPath(): void { - const result = findConfigFile({ options: {}, fileNames: [], errors: [] }); - Expect(result.isValid).toBe(false); - } -} diff --git a/test/unit/comments.spec.ts b/test/unit/comments.spec.ts new file mode 100644 index 000000000..2efb92b53 --- /dev/null +++ b/test/unit/comments.spec.ts @@ -0,0 +1,115 @@ +import * as util from "../util"; + +test("Single-line JSDoc is copied on a function", () => { + const builder = util.testModule` + /** This is a function comment. */ + function foo() {} + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("This is a function comment."); +}); + +test("Multi-line JSDoc with one block is copied on a function", () => { + const builder = util.testModule` + /** + * This is a function comment. + * It has more than one line. + */ + function foo() {} + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("It has more than one line."); +}); + +test("Multi-line JSDoc with two blocks is copied on a function", () => { + const builder = util.testModule` + /** + * This is a function comment. + * It has more than one line. + * + * It also has more than one block. + */ + function foo() {} + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("It also has more than one block."); +}); + +test("JSDoc is copied on a function with tags", () => { + const builder = util.testModule` + /** + * This is a function comment. + * It has multiple lines. + * + * @param arg1 This is the first argument. + * @param arg2 This is the second argument. + * @returns A very powerful string. + */ + function foo(arg1: boolean, arg2: number): string { + return "bar"; + } + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("This is the first argument."); + expect(lua).toContain("This is the second argument."); + expect(lua).toContain("A very powerful string."); +}); + +test("JSDoc is copied on a variable", () => { + const builder = util.testModule` + /** This is a variable comment. */ + const foo = 123; + ` + .expectToHaveNoDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("This is a variable comment."); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1299 +test("tsdoc comment does not cause a diagnostic (#1299)", () => { + util.testModule` + interface LuaBootstrap { + /** + * @remarks + * + * Links can point to a URL: {@link https://github.com/microsoft/tsdoc} + */ + on_init(f: (() => void) ): void + } + + const script : LuaBootstrap = { + on_init: (x) => x() + } + + script.on_init(() => {}) + `.expectToHaveNoDiagnostics(); +}); diff --git a/test/unit/compiler/configuration/mixed/index.spec.ts b/test/unit/compiler/configuration/mixed/index.spec.ts deleted file mode 100644 index 179a8ac77..000000000 --- a/test/unit/compiler/configuration/mixed/index.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Expect, Test } from "alsatian"; -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; - -import { CompilerOptions, LuaLibImportKind } from "../../../../../src/CompilerOptions"; -import { parseCommandLine } from "../../../../../src/CommandLineParser"; - -export class MixedConfigurationTests -{ - @Test("tsconfig.json mixed with cmd line args") - public tsconfigMixedWithCmdLineArgs(): void { - const rootPath = __dirname; - const tsConfigPath = path.join(rootPath, "project-tsconfig.json"); - const expectedTsConfig = ts.parseJsonConfigFileContent( - ts.parseConfigFileTextToJson(tsConfigPath, fs.readFileSync(tsConfigPath).toString()).config, - ts.sys, - path.dirname(tsConfigPath) - ); - - const parsedArgs = parseCommandLine([ - "-p", - `"${tsConfigPath}"`, - "--luaLibImport", - LuaLibImportKind.Inline, - `${path.join(rootPath, "test.ts")}`, - ]); - - if (parsedArgs.isValid === true) - { - Expect(parsedArgs.result.options).toEqual({ - ...expectedTsConfig.options, - // Overridden by cmd args (set to "none" in project-tsconfig.json) - luaLibImport: LuaLibImportKind.Inline, - // Only set in tsconfig, TSTL default is "JIT" - luaTarget: "5.1", - // Only present in TSTL dfaults - noHeader: false, - project: tsConfigPath, - noHoisting: false, - } as CompilerOptions); - } else { - Expect(parsedArgs.isValid).toBeTruthy(); - } - } -} diff --git a/test/unit/compiler/configuration/mixed/project-tsconfig.json b/test/unit/compiler/configuration/mixed/project-tsconfig.json deleted file mode 100644 index 3fe0b7e03..000000000 --- a/test/unit/compiler/configuration/mixed/project-tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist/foo/bar", - "rootDir": "./src/foo/bar" - }, - "luaTarget": "5.1", - "luaLibImport": "none" -} diff --git a/test/unit/compiler/configuration/options.spec.ts b/test/unit/compiler/configuration/options.spec.ts deleted file mode 100644 index 28324c10a..000000000 --- a/test/unit/compiler/configuration/options.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import * as util from "../../../src/util"; -import { LuaTarget, LuaLibImportKind } from "../../../../src/CompilerOptions"; - -export class ObjectLiteralTests -{ - @TestCase(LuaTarget.LuaJIT) - @TestCase("jit") - @TestCase("JiT") - @Test("Options LuaTarget case-insensitive") - public luaTargetCaseInsensitive(target: string): void { - const options = {LuaTarget: target}; - const result = util.transpileString("~a", options); - - Expect(result).toBe("bit.bnot(a);"); - } - - @TestCase(LuaLibImportKind.None) - @TestCase("none") - @TestCase("NoNe") - @Test("Options LuaLibImport case-insensitive") - public luaLibImportCaseInsensitive(importKind: string): void { - const options = {LuaLibImportKind: importKind}; - const result = util.transpileString("const a = new Map();", options); - - Expect(result).toBe("local a = Map.new();"); - } -} diff --git a/test/unit/conditionals.spec.ts b/test/unit/conditionals.spec.ts index 6d330100b..f862ca8a3 100644 --- a/test/unit/conditionals.spec.ts +++ b/test/unit/conditionals.spec.ts @@ -1,402 +1,208 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { TranspileError } from "../../src/TranspileError"; -import { LuaTarget } from "../../src/CompilerOptions"; -import * as util from "../src/util"; - -export class LuaConditionalsTests { - - @TestCase(0, 0) - @TestCase(1, 1) - @Test("if") - public if(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let input: number = ${inp}; - if (input === 0) { - return 0; - } - return 1;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @Test("ifelse") - public ifelse(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let input: number = ${inp}; - if (input === 0) { - return 0; - } else { - return 1; - }` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, 3) - @Test("ifelseif") - public ifelseif(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let input: number = ${inp}; - if (input === 0) { - return 0; - } else if (input === 1){ - return 1; - } else if (input === 2){ - return 2; - } - return 3;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, 3) - @Test("ifelseifelse") - public ifelseifelse(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let input: number = ${inp}; - if (input === 0) { - return 0; - } else if (input === 1){ - return 1; - } else if (input === 2){ - return 2; - } else { - return 3; - }` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, -1) - @Test("switch") - public switch(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: - result = 0; - break; - case 1: - result = 1; - break; - case 2: - result = 2; - break; - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, -2) - @Test("switchdefault") - public switchdefault(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: - result = 0; - break; - case 1: - result = 1; - break; - case 2: - result = 2; - break; - default: - result = -2; - break; - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 1) - @TestCase(0, 1) - @TestCase(2, 4) - @TestCase(3, 4) - @TestCase(4, 4) - @TestCase(5, 15) - @TestCase(7, -2) - @Test("switchfallthrough") - public switchfallthrough(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: - result = 0; - case 1: - result = 1; - break; - case 2: - result = 2; - case 3: - case 4: - result = 4; - break; - case 5: - result = 5; - case 6: - result += 10; - break; - case 7: - result = 7; - default: - result = -2; - break; - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, -2) - @Test("nestedSwitch") - public nestedSwitch(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: - result = 0; - break; - case 1: - switch(${inp}) { - case 0: - result = 0; - break; - case 1: - result = 1; - break; - default: - result = -3; - break; - } - break; - case 2: - result = 2; - break; - default: - result = -2; - break; - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 2) - @TestCase(2, 2) - @Test("switchLocalScope") - public switchLocalScope(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: { - let x = 0; - result = 0; - break; - } - case 1: { - let x = 1; - result = x; - } - case 2: { - let x = 2; - result = x; - break; - } - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, -1) - @Test("switchReturn") - public switchReturn(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `const result: number = -1; - - switch (${inp}) { - case 0: - return 0; - break; - case 1: - return 1; - case 2: - return 2; - break; - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, -1) - @Test("switchWithBrackets") - public switchWithBrackets(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: { - result = 0; - break; - } - case 1: { - result = 1; - break; - } - case 2: { - result = 2; - break; - } - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - - @TestCase(0, 0) - @TestCase(1, 1) - @TestCase(2, 2) - @TestCase(3, -1) - @Test("switchWithBracketsBreakInConditional") - public switchWithBracketsBreakInConditional(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: { - result = 0; - break; - } - case 1: { - result = 1; - - if (result == 1) break; - } - case 2: { - result = 2; - break; - } - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase(0, 4) - @TestCase(1, 0) - @TestCase(2, 2) - @TestCase(3, -1) - @Test("switchWithBracketsBreakInInternalLoop") - public switchWithBracketsBreakInInternalLoop(inp: number, expected: number): void { - const result = util.transpileAndExecute( - `let result: number = -1; - - switch (${inp}) { - case 0: { - result = 0; - - for (let i = 0; i < 5; i++) { - result++; - - if (i >= 2) { - break; - } - } - } - case 1: { - result++; - break; - } - case 2: { - result = 2; - break; - } - } - return result;` - ); - - // Assert - Expect(result).toBe(expected); - } - - @Test("If dead code after return") - public ifDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `if (true) { return 3; const b = 8; }`); - - Expect(result).toBe(3); - } - - @Test("switch dead code after return") - public whileDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `switch ("abc") { case "def": return 4; let abc = 4; case "abc": return 5; let def = 6; }`); - - Expect(result).toBe(5); - } - - @Test("switch not allowed in 5.1") - public switchThrow51(): void { - Expect( () => util.transpileString(`switch ("abc") {}`, {luaTarget: LuaTarget.Lua51})) - .toThrowError(TranspileError, "Switch statements is/are not supported for target Lua 5.1."); - } -} +import * as tstl from "../../src"; +import * as util from "../util"; +import { truthyOnlyConditionalValue } from "../../src/transformation/utils/diagnostics"; + +test.each([0, 1])("if (%p)", inp => { + util.testFunction` + let input: number = ${inp}; + if (input === 0) { + return 0; + } + return 1; + `.expectToMatchJsResult(); +}); + +test.each([0, 1])("ifelse (%p)", inp => { + util.testFunction` + let input: number = ${inp}; + if (input === 0) { + return 0; + } else { + return 1; + } + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("ifelseif (%p)", inp => { + util.testFunction` + let input: number = ${inp}; + if (input === 0) { + return 0; + } else if (input === 1){ + return 1; + } else if (input === 2){ + return 2; + } + return 3; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("ifelseifelse (%p)", inp => { + util.testFunction` + let input: number = ${inp}; + if (input === 0) { + return 0; + } else if (input === 1){ + return 1; + } else if (input === 2){ + return 2; + } else { + return 3; + } + `.expectToMatchJsResult(); +}); + +test.each([ + { input: "true ? 'a' : 'b'" }, + { input: "false ? 'a' : 'b'" }, + { input: "true ? false : true" }, + { input: "false ? false : true" }, + { input: "true || true ? 'a' : 'b'" }, + { input: "true || false ? 'a' : 'b'" }, + { input: "false || true ? 'a' : 'b'" }, + { input: "false || false ? 'a' : 'b'" }, + { input: "true ? literalValue : true" }, + { input: "true ? variableValue : true" }, + { input: "true ? maybeUndefinedValue : true" }, + { input: "true ? maybeBooleanValue : true" }, + { input: "true ? maybeUndefinedValue : true", options: { strictNullChecks: true } }, + { input: "true ? maybeBooleanValue : true", options: { strictNullChecks: true } }, + { input: "true ? undefined : true", options: { strictNullChecks: true } }, + { input: "true ? null : true", options: { strictNullChecks: true } }, + { input: "true ? false : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "false ? false : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, +])("Ternary operator (%p)", ({ input, options }) => { + util.testFunction` + const literalValue = "literal"; + let variableValue: string; + let maybeBooleanValue: string | boolean = false; + let maybeUndefinedValue: string | undefined; + return ${input}; + ` + .setOptions(options) + .expectToMatchJsResult(); +}); + +test.each([ + { condition: true, lhs: 4, rhs: 5 }, + { condition: false, lhs: 4, rhs: 5 }, + { condition: 3, lhs: 4, rhs: 5 }, +])("Ternary Conditional (%p)", ({ condition, lhs, rhs }) => { + util.testExpressionTemplate`${condition} ? ${lhs} : ${rhs}` + .ignoreDiagnostics([ + truthyOnlyConditionalValue.code, + 2872 /* TS2872: This kind of expression is always truthy. */, + ]) + .expectToMatchJsResult(); +}); + +test.each(["true", "false", "a < 4", "a == 8"])("Ternary Conditional Delayed (%p)", condition => { + util.testFunction` + let a = 3; + let delay = () => ${condition} ? a + 3 : a + 5; + a = 8; + return delay(); + `.expectToMatchJsResult(); +}); + +test.each([false, true, null])("Ternary conditional with generic whenTrue branch (%p)", trueVal => { + util.testFunction` + function ternary(a: boolean, b: B, c: C) { + return a ? b : c + } + return ternary(true, ${trueVal}, "wasFalse") + ` + .setOptions({ + strictNullChecks: true, + }) + .expectToMatchJsResult(); +}); + +test.each([false, true])("Ternary conditional with preceding statements in true branch (%p)", trueVal => { + // language=TypeScript + util.testFunction` + let i = 0; + const result = ${trueVal} ? i += 1 : i; + return { result, i }; + ` + .setOptions({ + strictNullChecks: true, + }) + .expectToMatchJsResult(); +}); + +test.each([false, true])("Ternary conditional with preceding statements in false branch (%p)", trueVal => { + // language=TypeScript + util.testFunction` + let i = 0; + const result = ${trueVal} ? i : i += 2; + return { result, i }; + ` + .setOptions({ + strictNullChecks: true, + }) + .expectToMatchJsResult(); +}); + +test.each(["string", "number", "string | number"])( + "Warning when using if statement that cannot evaluate to false undefined or null (%p)", + type => { + util.testFunction` + if (condition) {} + ` + .setTsHeader(`declare var condition: ${type};`) + .setOptions({ strict: true }) + .expectToHaveDiagnostics([truthyOnlyConditionalValue.code]); + } +); + +test.each(["string", "number", "string | number"])("Warning can be disabled when strict is true (%p)", type => { + util.testFunction` + if (condition) {} + ` + .setTsHeader(`declare var condition: ${type};`) + .setOptions({ strict: true, strictNullChecks: false }) + .expectToHaveNoDiagnostics(); +}); + +test.each(["string", "number", "string | number"])( + "Warning when using while statement that cannot evaluate to false undefined or null (%p)", + type => { + util.testFunction` + while (condition) {} + ` + .setTsHeader(`declare var condition: ${type};`) + .setOptions({ strict: true }) + .expectToHaveDiagnostics([truthyOnlyConditionalValue.code]); + } +); + +test.each(["string", "number", "string | number"])( + "Warning when using do while statement that cannot evaluate to false undefined or null (%p)", + type => { + util.testFunction` + do {} while (condition) + ` + .setTsHeader(`declare var condition: ${type};`) + .setOptions({ strict: true }) + .expectToHaveDiagnostics([truthyOnlyConditionalValue.code]); + } +); + +test.each(["string", "number", "string | number"])( + "Warning when using ternary that cannot evaluate to false undefined or null (%p)", + type => { + util.testExpression`condition ? 1 : 0` + .setTsHeader(`declare var condition: ${type};`) + .setOptions({ strict: true }) + .expectToHaveDiagnostics([truthyOnlyConditionalValue.code]); + } +); + +test.each(["string", "number", "string | number"])("No warning when using element index in condition (%p)", type => { + util.testExpression`condition[0] ? 1 : 0` + .setTsHeader(`declare var condition: ${type}[];`) + .setOptions({ strict: true }) + .expectToHaveNoDiagnostics(); +}); diff --git a/test/unit/curry.spec.ts b/test/unit/curry.spec.ts deleted file mode 100644 index 03bf99000..000000000 --- a/test/unit/curry.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; - -export class LuaCurryTests { - - @Test("curryingAdd") - @TestCase(2, 3) - @TestCase(5, 4) - public curryingAdd(x: number, y: number): void - { - const result = util.transpileAndExecute( - `let add = (x: number) => (y: number) => x + y; - return add(${x})(${y})` - ); - - // Assert - Expect(result).toBe(x + y); - } -} diff --git a/test/unit/declarations.spec.ts b/test/unit/declarations.spec.ts deleted file mode 100644 index ae7531aba..000000000 --- a/test/unit/declarations.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; - -export class DeclarationTests -{ - @Test("Declaration function call") - public declarationFunctionCall(): void { - // Arrange - const libLua = `function declaredFunction(x) return 3*x end`; - - const tsHeader = `declare function declaredFunction(x: number): number;`; - - const source = `return declaredFunction(2) + 4;`; - - // Act - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - - // Assert - Expect(result).toBe(10); - } - - @Test("Declaration function call tupleReturn") - public declarationFunctionCallTupleReturn(): void { - // Arrange - const libLua = `function declaredFunction(x) return x, 2*x + 1 end`; - - const tsHeader = ` - /** @tupleReturn */ - declare function declaredFunction(x: number): [number, number];`; - - const source = ` - const tuple = declaredFunction(3); - const [destructedLeft, destructedRight] = declaredFunction(2); - return \`\${tuple[0] + destructedLeft},\${tuple[1] + destructedRight}\`;`; - - // Act - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - - // Assert - Expect(result).toBe("5,12"); - } - - @Test("Declaration namespace function call") - public declarationNamespaceFunctionCall(): void { - // Arrange - const libLua = ` - myNameSpace = {} - function myNameSpace.declaredFunction(x) return 3*x end - `; - - const tsHeader = `declare namespace myNameSpace { function declaredFunction(x: number): number; }`; - - const source = `return myNameSpace.declaredFunction(2) + 4;`; - - // Act - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - - // Assert - Expect(result).toBe(10); - } - - @Test("Declaration interface function call") - public declarationInterfaceFunctionCall(): void { - // Arrange - const libLua = ` - myInterfaceInstance = {} - myInterfaceInstance.x = 10 - function myInterfaceInstance:declaredFunction(x) return self.x + 3*x end - `; - - const tsHeader = ` - declare interface MyInterface { - declaredFunction(x: number): number; - } - declare var myInterfaceInstance: MyInterface;`; - - const source = `return myInterfaceInstance.declaredFunction(3);`; - - // Act - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - - // Assert - Expect(result).toBe(19); - } - - @Test("Declaration function callback") - public declarationFunctionCallback(): void { - // Arrange - const libLua = `function declaredFunction(callback) return callback(4) end`; - const tsHeader = `declare function declaredFunction(callback: (x: number) => number): number;`; - - const source = `return declaredFunction(x => 2 * x);`; - - // Act - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - - // Assert - Expect(result).toBe(8); - } - - @Test("Declaration instance function callback") - public declarationInstanceFunctionCallback(): void { - // Arrange - const libLua = ` - myInstance = {} - myInstance.x = 10 - function myInstance:declaredFunction(callback) return callback(self.x) end`; - - const tsHeader = - `declare interface MyInterface { - declaredFunction(callback: (x: number) => number): number; - } - declare var myInstance: MyInterface;`; - - const source = `return myInstance.declaredFunction(x => 2 * x);`; - - // Act - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - - // Assert - Expect(result).toBe(20); - } -} diff --git a/test/unit/decoratorCustomConstructor.spec.ts b/test/unit/decoratorCustomConstructor.spec.ts deleted file mode 100644 index 69fb6bd2a..000000000 --- a/test/unit/decoratorCustomConstructor.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; - -import { TranspileError } from "../../src/TranspileError"; - -export class DecoratorCustomConstructor -{ - @Test("CustomCreate") - public customCreate(): void { - const luaHeader = - `function Point2DCreate(x, y) - return {x = x, y = y} - end`; - - const tsHeader = - `/** @customConstructor Point2DCreate */ - class Point2D { - public x: number; - public y: number; - constructor(x: number, y: number) { - // No values assigned - } - }`; - - const result = util.transpileAndExecute( - `return new Point2D(1, 2).x;`, - undefined, - luaHeader, - tsHeader - ); - - // Assert - Expect(result).toBe(1); - } - - @Test("IncorrectUsage") - public incorrectUsage(): void { - Expect(() => { - util.transpileString( - `/** @customConstructor */ - class Point2D { - constructor( - public x: number, - public y: number - ) {} - } - return new Point2D(1, 2).x; - ` - ); - }).toThrowError(TranspileError, "!CustomConstructor expects 1 argument(s) but got 0."); - } -} diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts deleted file mode 100644 index 6fdedf191..000000000 --- a/test/unit/decoratorMetaExtension.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Expect, Test } from "alsatian"; -import * as util from "../src/util"; - -import { TranspileError } from "../../src/TranspileError"; -import { TSTLErrors } from "../../src/TSTLErrors"; - -export class DecoratorMetaExtension { - - @Test("MetaExtension") - public metaExtension(): void { - const tsHeader = ` - declare class _LOADED {} - declare namespace debug { - function getregistry(): any; - } - /** @metaExtension */ - class LoadedExt extends _LOADED { - public static test() { - return 5; - } - }`; - - const result = util.transpileAndExecute( - `return debug.getregistry()["_LOADED"].test();`, - undefined, undefined, tsHeader); - - // Assert - Expect(result).toBe(5); - } - - @Test("IncorrectUsage") - public incorrectUsage(): void { - const expectedMessage = TSTLErrors.MissingMetaExtension(undefined).message; - Expect(() => { - util.transpileString( - ` - /** @metaExtension */ - class LoadedExt { - public static test() { - return 5; - } - } - ` - ); - }).toThrowError(TranspileError, expectedMessage); - } - - @Test("DontAllowInstantiation") - public dontAllowInstantiation(): void { - Expect(() => { - util.transpileString( - ` - declare class _LOADED {} - /** @metaExtension */ - class Ext extends _LOADED { - } - const e = new Ext(); - ` - ); - }).toThrowError(TranspileError, - "Cannot construct classes with decorator '@extension' or '@metaExtension'."); - } -} diff --git a/test/unit/destructuring.spec.ts b/test/unit/destructuring.spec.ts new file mode 100644 index 000000000..65ecf95a3 --- /dev/null +++ b/test/unit/destructuring.spec.ts @@ -0,0 +1,250 @@ +import { cannotAssignToNodeOfKind, invalidMultiReturnAccess } from "../../src/transformation/utils/diagnostics"; +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) })); + +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 }) => { + util.testFunction` + let ${allBindings}; + function test(${binding}) { + return { ${allBindings} }; + } + + return test(${value}); + `.expectToMatchJsResult(); +}); + +test("in function parameter creates local variables", () => { + const builder = util.testModule` + function test({ a, b }: any) {} + `; + + const code = builder.getMainLuaCodeChunk(); + expect(code).toContain("local a ="); + expect(code).toContain("local b ="); +}); + +test("in function parameter creates local variables in correct scope", () => { + util.testFunction` + let x = 7; + function foo([x]: [number]) { + x *= 2; + } + foo([1]); + return x; + `.expectToMatchJsResult(); +}); + +test.each(testCases)("in variable declaration (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + { + const ${binding} = ${value}; + return { ${allBindings} }; + } + `.expectToMatchJsResult(); +}); + +test.each(testCases)("in variable declaration from const variable (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + { + const v: any = ${value}; + const ${binding} = v; + return { ${allBindings} }; + } + `.expectToMatchJsResult(); +}); + +test.each(testCases)("in variable declaration from this (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + function test(this: any) { + const ${binding} = this; + return { ${allBindings} }; + } + return test.call(${value}); + `.expectToMatchJsResult(); +}); + +test.each(testCases)("in exported variable declaration (%p)", ({ binding, value }) => { + util.testModule` + export const ${binding} = ${value}; + `.expectToMatchJsResult(); +}); + +const assignmentTestCases = [ + ...testCases, + ...[ + { binding: "{ x: obj.prop }", value: { x: true } }, + { binding: "{ x: obj.prop = true }", value: {} }, + { binding: "[{ x: obj.prop }]", value: [{ x: true }] }, + { binding: "{ obj: { prop: obj.prop } }", value: { obj: { prop: true } } }, + { binding: "{ x = true }", value: {} }, + ].map(({ binding, value }) => ({ binding, value: util.formatCode(value) })), + { binding: "{ x: { [(3).toString()]: y } }", value: "{ x: { [(3).toString()]: true } }" }, +]; + +test.each(assignmentTestCases)("in assignment expression (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + const obj = { prop: false }; + const expressionResult = (${binding} = ${value}); + return { ${allBindings}, obj, expressionResult }; + `.expectToMatchJsResult(); +}); + +test.each(assignmentTestCases)("in assignment expression from const variable (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + const obj = { prop: false }; + const v: any = ${value}; + const expressionResult = (${binding} = v); + return { ${allBindings}, expressionResult }; + `.expectToMatchJsResult(); +}); + +test.each(assignmentTestCases)("in assignment expression from this (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + const obj = { prop: false }; + function test(this: any) { + const expressionResult = (${binding} = this); + return { ${allBindings}, obj, expressionResult }; + } + return test.call(${value}); + `.expectToMatchJsResult(); +}); + +test.each(["[]", "{}"])("empty binding pattern", bindingPattern => { + util.testFunction` + let i = 1; + const ${bindingPattern} = [i++]; + return i; + `.expectToMatchJsResult(); +}); + +// TODO: https://github.com/microsoft/TypeScript/pull/35906 +// Adjust this test to use expectToMatchJsResult() and testCases when this issue is fixed. +test.each([ + ["foo", "['bar']"], + ["[foo]", "[['bar']]"], + ["[foo = 'bar']", "[[]]"], + ["{ foo }", "[{ foo: 'bar' }]"], + ["{ x: foo }", "[{ x: 'bar' }]"], + ["{ foo = 'bar' }", "[{}] as { foo?: string }[]"], +])("forof assignment updates dependencies", (initializer, expression) => { + util.testModule` + let foo = ''; + export { foo }; + for (${initializer} of ${expression}) {} + ` + .setReturnExport("foo") + .expectToEqual("bar"); +}); + +test.each(testCases)("forof variable declaration binding patterns (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + for (const ${binding} of [${value} as any]) { + return { ${allBindings} }; + } + `.expectToMatchJsResult(); +}); + +describe("array destructuring optimization", () => { + // TODO: Try to generalize optimization logic between declaration and assignment and make more generic tests + + test("array", () => { + util.testFunction` + const array = [3, 5, 1]; + const [a, b, c] = array; + return { a, b, c }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("unpack")) + .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]; + return { a, b, c }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("unpack")) + .expectToMatchJsResult(); + }); + + test("array literal with extra values", () => { + util.testFunction` + let called = false; + const set = () => { called = true; }; + const [head] = ["foo", set()]; + return { head, called }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("unpack")) + .expectToMatchJsResult(); + }); + + test("array union", () => { + util.testFunction` + const array: [string] | [] = ["bar"]; + let x: string; + [x] = array; + return x; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("unpack")) + .expectToMatchJsResult(); + }); +}); + +test("no exception from semantically invalid TS", () => { + util.testModule` + declare function testFunc(value: number): LuaMultiReturn<[number, number]>; + let [a, b] = testFunc(5) // Missing ; + [a, b] = testFunc(b) // Interpreted as testFunc(5)[a, b] + ` + .withLanguageExtensions() + .disableSemanticCheck() + .expectToHaveDiagnostics([invalidMultiReturnAccess.code, cannotAssignToNodeOfKind.code]); +}); diff --git a/test/unit/enum.spec.ts b/test/unit/enum.spec.ts index d21b695c4..3ff4ce3fe 100644 --- a/test/unit/enum.spec.ts +++ b/test/unit/enum.spec.ts @@ -1,191 +1,216 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; - -import { TranspileError } from "../../src/TranspileError"; - -export class EnumTests { - @Test("Declare const enum") - public declareConstEnum(): void { - const testCode = ` - declare const enum TestEnum { - MEMBER_ONE = "test", - MEMBER_TWO = "test2" - } - - const valueOne = TestEnum.MEMBER_ONE; - `; +import * as util from "../util"; - Expect(util.transpileString(testCode)).toBe(`local valueOne = "test";`); +// TODO: string.toString() +const serializeEnum = (identifier: string) => `(() => { + const mappedTestEnum: any = {}; + for (const key in ${identifier}) { + mappedTestEnum[(key as any).toString()] = ${identifier}[key]; } - - @Test("Const enum") - public constEnum(): void { - const testCode = ` - const enum TestEnum { - MEMBER_ONE = "test", - MEMBER_TWO = "test2" + return mappedTestEnum; +})()`; + +describe("initializers", () => { + test("string", () => { + util.testFunction` + enum TestEnum { + A = "A", + B = "B", } - const valueOne = TestEnum.MEMBER_TWO; - `; + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); - Expect(util.transpileString(testCode)).toBe(`local valueOne = "test2";`); - } - - @Test("Const enum without initializer") - public constEnumNoInitializer(): void { - const testCode = ` - const enum TestEnum { - MEMBER_ONE, - MEMBER_TWO + test("expression", () => { + util.testFunction` + const value = 6; + enum TestEnum { + A, + B = value, } - const valueOne = TestEnum.MEMBER_TWO; - `; + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); - Expect(util.transpileString(testCode)).toBe(`local valueOne = 1;`); - } - - @Test("Const enum without initializer in some values") - public constEnumNoInitializerInSomeValues(): void { - const testCode = ` - const enum TestEnum { - MEMBER_ONE = 3, - MEMBER_TWO, - MEMBER_THREE = 5 + test("expression with side effect", () => { + util.testFunction` + let value = 0; + enum TestEnum { + A = value++, + B = A, } - const valueOne = TestEnum.MEMBER_TWO; - `; - - Expect(util.transpileString(testCode)).toBe(`local valueOne = 4;`); - } - - @Test("Invalid heterogeneous enum") - public invalidHeterogeneousEnum(): void { - // Transpile & Assert - Expect(() => { - const lua = util.transpileString( - `enum TestEnum { - a, - b = "ok", - c, - }` - ); - }).toThrowError(TranspileError, "Invalid heterogeneous enum. Enums should either specify no " - + "member values, or specify values (of the same type) for all members."); - } - - @Test("String literal name in enum") - public stringLiteralNameEnum(): void { - const code = `enum TestEnum { - ["name"] = "foo" - } - return TestEnum["name"];`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); - @Test("Enum identifier value internal") - public enumIdentifierValueInternal(): void { - const result = util.transpileAndExecute( - `enum testEnum { - abc, - def, - ghi = def, - jkl, + test("inference", () => { + util.testFunction` + enum TestEnum { + A, + B, + C, } - return \`\${testEnum.abc},\${testEnum.def},\${testEnum.ghi},\${testEnum.jkl}\`;` - ); - Expect(result).toBe("0,1,1,2"); - } + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); - @Test("Enum identifier value internal recursive") - public enumIdentifierValueInternalRecursive(): void { - const result = util.transpileAndExecute( - `enum testEnum { - abc, - def, - ghi = def, - jkl = ghi, + test("partial inference", () => { + util.testFunction` + enum TestEnum { + A = 3, + B, + C = 5, } - return \`\${testEnum.abc},\${testEnum.def},\${testEnum.ghi},\${testEnum.jkl}\`;` - ); - Expect(result).toBe("0,1,1,1"); - } + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); - @Test("Enum identifier value external") - public enumIdentifierValueExternal(): void { - const result = util.transpileAndExecute( - `const ext = 6; - enum testEnum { - abc, - def, - ghi = ext, + test("member reference", () => { + util.testFunction` + enum TestEnum { + A, + B = A, + C = B, } - return \`\${testEnum.abc},\${testEnum.def},\${testEnum.ghi}\`;` - ); - Expect(result).toBe("0,1,6"); - } + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); - @Test("Enum reverse mapping") - public enumReverseMapping(): void { - const result = util.transpileAndExecute( - `enum testEnum { - abc, - def, - ghi + test("string literal member reference", () => { + util.testFunction` + enum TestEnum { + ["A"], + "B" = A, + C = B, } - return testEnum[testEnum.abc] + testEnum[testEnum.ghi]` - ); - Expect(result).toBe("abcghi"); - } + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); +}); - @Test("Const enum index") - public constEnumIndex(): void { - const result = util.transpileAndExecute( - `const enum testEnum { - abc, - def, - ghi +describe("const enum", () => { + const expectToBeConst: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).not.toContain("TestEnum"); + + test.each(["", "declare "])("%swithout initializer", modifier => { + util.testModule` + ${modifier} const enum TestEnum { + A, + B, } - return testEnum["def"];` - ); - Expect(result).toBe(1); - } + export const A = TestEnum.A; + ` + .tap(expectToBeConst) + .expectToMatchJsResult(); + }); - @Test("Const enum index identifier value") - public constEnumIndexIdnetifierValue(): void { - const result = util.transpileAndExecute( - `const enum testEnum { - abc, - def = 4, - ghi, - jkl = ghi + test("with string initializer", () => { + util.testFunction` + const enum TestEnum { + A = "ONE", + B = "TWO", } - return testEnum["jkl"];` - ); - Expect(result).toBe(5); - } + return TestEnum.A; + ` + .tap(expectToBeConst) + .expectToMatchJsResult(); + }); - @Test("Const enum index identifier chain") - public constEnumIndexIdnetifierChain(): void { - const result = util.transpileAndExecute( - `const enum testEnum { - abc = 3, - def, - ghi = def, - jkl = ghi, + test("access with string literal", () => { + util.testFunction` + const enum TestEnum { + A, + B, + C, } - return testEnum["ghi"];` - ); - Expect(result).toBe(4); - } -} + return TestEnum["C"]; + ` + .tap(expectToBeConst) + .expectToMatchJsResult(); + }); +}); + +test("toString", () => { + util.testFunction` + enum TestEnum { + A, + B, + C, + } + + function foo(value: TestEnum) { + return value.toString(); + } + + return foo(TestEnum.A); + `.expectToMatchJsResult(); +}); + +test("enum merging", () => { + util.testFunction` + enum TestEnum { + A, B + } + + enum TestEnum { + C = 3, + D + } + + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); +}); + +test("enum merging with overlap", () => { + util.testFunction` + enum TestEnum { + A, B + } + + enum TestEnum { + C = 1, + D + } + + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); +}); + +test("enum merging multiple files", () => { + util.testModule` + import "./otherfile" + enum TestEnum { + A, B + } + + export default ${serializeEnum("TestEnum")} + ` + .addExtraFile( + "otherfile.ts", + `enum TestEnum { + C = 3, + D + }` + ) + .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 a7058bc03..05399d492 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -1,78 +1,370 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; +import * as util from "../util"; +import * as tstl from "../../src"; -import { TranspileError } from "../../src/TranspileError"; +test("throwString", () => { + util.testFunction` + throw "Some Error" + `.expectToEqual(new util.ExecutionError("Some Error")); +}); -export class LuaErrorTests { - - @Test("throwString") - public trowString(): void { - // Transpile - const lua = util.transpileString( - `throw "Some Error"` - ); - // Assert - Expect(lua).toBe(`error("Some Error");`); - } - - @Test("throwError") - public throwError(): void { - // Transpile & Asser - Expect(() => { - const lua = util.transpileString( - `throw Error("Some Error")` - ); - }).toThrowError(TranspileError, "Invalid throw expression, only strings can be thrown."); - } - - @TestCase(0, "A") - @TestCase(1, "B") - @TestCase(2, "C") - @Test("re-throw") - public reThrow(i: number, expected: any): void { - const source = - `const i: number = ${i}; - function foo() { +// 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 => { + util.testFunction` + const i: number = ${i}; + function foo() { + try { try { - try { - if (i === 0) { throw "z"; } - } catch (e) { - throw "a"; - } finally { - if (i === 1) { throw "b"; } - } + if (i === 0) { throw "z"; } } catch (e) { - throw (e as string).toUpperCase(); + throw "a"; } finally { - throw "C"; + if (i === 1) { throw "b"; } } + } catch (e) { + throw (e as string).toUpperCase(); + } finally { + throw "C"; } - let result: string = "x"; + } + let result: string = "x"; + try { + foo(); + } catch (e) { + result = (e as string)[(e as string).length - 1]; + } + return result; + `.expectToMatchJsResult(); +}); + +test("re-throw (no catch var)", () => { + util.testFunction` + let result = "x"; + try { + try { + throw "y"; + } catch { + throw "z"; + } + } catch (e) { + result = (e as string)[(e as string).length - 1]; + } + return result; + `.expectToMatchJsResult(); +}); + +test("return from try", () => { + util.testFunction` + function foobar() { + try { + return "foobar"; + } catch { + } + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("return nil from try", () => { + util.testFunction` + let x = "unset"; + function foobar() { + try { + return; + } catch { + } + x = "set"; + } + foobar(); + return x; + `.expectToMatchJsResult(); +}); + +test("multi return from try", () => { + const testBuilder = util.testFunction` + function foobar() { try { - foo(); + return $multi("foo", "bar"); + } catch { + } + } + const [foo, bar] = foobar(); + return foo + bar; + `.withLanguageExtensions(); + expect(testBuilder.getMainLuaCodeChunk()).not.toMatch("unpack(foobar"); + testBuilder.expectToMatchJsResult(); +}); + +test("return from catch", () => { + util.testFunction` + function foobar() { + try { + throw "foobar"; } catch (e) { - result = (e as string)[(e as string).length - 1]; + return e + " catch"; } - return result;`; - const result = util.transpileAndExecute(source); - Expect(result).toBe(expected); - } + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("return nil from catch", () => { + util.testFunction` + let x = "unset"; + function foobar() { + try { + throw "foobar"; + } catch (e) { + return; + } + x = "set"; + } + foobar(); + return x; + `.expectToMatchJsResult(); +}); - @Test("re-throw (no catch var)") - public reThrowWithoutCatchVar(): void { - const source = - `let result = "x"; +test("multi return from catch", () => { + const testBuilder = util.testFunction` + function foobar(): LuaMultiReturn<[string, string]> { + try { + throw "foobar"; + } catch (e) { + return $multi(e.toString(), " catch"); + } + } + const [foo, bar] = foobar(); + return foo + bar; + `.withLanguageExtensions(); + expect(testBuilder.getMainLuaCodeChunk()).not.toMatch("unpack(foobar"); + testBuilder.expectToMatchJsResult(); +}); + +test("return from nested try", () => { + util.testFunction` + function foobar() { try { try { - throw "y"; + return "foobar"; } catch { - throw "z"; } + } catch { + } + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("return from nested catch", () => { + util.testFunction` + function foobar() { + try { + throw "foobar"; + } catch (e) { + try { + throw e + " catch1"; + } catch (f) { + return f + " catch2"; + } + } + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("return from try->finally", () => { + util.testFunction` + let x = "unevaluated"; + function evaluate(arg: unknown) { + x = "evaluated"; + return arg; + } + function foobar() { + try { + return evaluate("foobar"); + } catch { + } finally { + return "finally"; + } + } + return foobar() + " " + x; + `.expectToMatchJsResult(); +}); + +test("return from catch->finally", () => { + util.testFunction` + let x = "unevaluated"; + function evaluate(arg: unknown) { + x = "evaluated"; + return arg; + } + function foobar() { + try { + throw "foobar"; } catch (e) { - result = (e as string)[(e as string).length - 1]; + return evaluate(e); + } finally { + return "finally"; } - return result;`; - const result = util.transpileAndExecute(source); - Expect(result).toBe("z"); + } + return foobar() + " " + x; + `.expectToMatchJsResult(); +}); + +test("multi return from try->finally", () => { + util.testFunction` + let x = "unevaluated"; + function evaluate(arg: string) { + x = "evaluated"; + return arg; + } + function foobar() { + try { + return $multi(evaluate("foo"), "bar"); + } catch { + } finally { + return $multi("final", "ly"); + } + } + const [foo, bar] = foobar(); + return foo + bar + " " + x; + ` + .withLanguageExtensions() + .expectToMatchJsResult(); +}); + +test("multi return from catch->finally", () => { + util.testFunction` + let x = "unevaluated"; + function evaluate(arg: string) { + x = "evaluated"; + return arg; + } + function foobar() { + try { + throw "foo"; + } catch (e) { + return $multi(evaluate(e), "bar"); + } finally { + return $multi("final", "ly"); + } + } + const [foo, bar] = foobar(); + return foo + bar + " " + x; + ` + .withLanguageExtensions() + .expectToMatchJsResult(); +}); + +test("return from nested finally", () => { + util.testFunction` + let x = ""; + function foobar() { + try { + try { + } finally { + x += "A"; + return "foobar"; + } + } finally { + x += "B"; + return "finally"; + } + } + return foobar() + " " + x; + `.expectToMatchJsResult(); +}); + +test.each([ + '"error string"', + "42", + "3.141", + "true", + "false", + "undefined", + '{ x: "error object" }', + '() => "error function"', +])("throw and catch %s", error => { + util.testFunction` + try { + throw ${error}; + } catch (error) { + if (typeof error == 'function') { + return error(); + } else { + return error; + } + } + `.expectToMatchJsResult(); +}); + +const builtinErrors = ["Error", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError"]; + +test.each([...builtinErrors, ...builtinErrors.map(type => `new ${type}`)])("%s properties", errorType => { + util.testFunction` + const error = ${errorType}(); + return { name: error.name, message: error.message, string: error.toString() }; + `.expectToMatchJsResult(); +}); + +test.each([...builtinErrors, "CustomError"])("get stack from %s", errorType => { + const stack = util.testFunction` + class CustomError extends Error { + public name = "CustomError"; + } + + let stack: string | undefined; + + function innerFunction() { stack = new ${errorType}().stack; } + function outerFunction() { innerFunction(); } + outerFunction(); + + return stack; + `.getLuaExecutionResult(); + + expect(stack).toMatch("innerFunction"); + expect(stack).toMatch("outerFunction"); +}); + +test("still works without debug module", () => { + util.testFunction` + try + { + throw new Error("hello, world"); + } + catch (e) + { + return e; + } + ` + .setLuaHeader("debug = nil") + .expectToEqual({ + message: "hello, world", + name: "Error", + stack: undefined, + }); +}); + +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 5499ae374..ee0eab5f4 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -1,522 +1,212 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { TranspileError } from "../../src/TranspileError"; -import { LuaTarget, LuaLibImportKind } from "../../src/CompilerOptions"; - -import * as ts from "typescript"; -import * as util from "../src/util"; - -export class ExpressionTests { - - @TestCase("i++", "i = i + 1;") - @TestCase("++i", "i = i + 1;") - @TestCase("i--", "i = i - 1;") - @TestCase("--i", "i = i - 1;") - @TestCase("!a", "not a;") - @TestCase("-a", "-a;") - @TestCase("+a", "a;") - @TestCase("let a = delete tbl['test']", "local a = (function()\n tbl.test = nil;\n return true;\nend)();") - @TestCase("delete tbl['test']", "tbl.test = nil;") - @TestCase("let a = delete tbl.test", "local a = (function()\n tbl.test = nil;\n return true;\nend)();") - @TestCase("delete tbl.test", "tbl.test = nil;") - @Test("Unary expressions basic") - public unaryBasic(input: string, lua: string): void { - Expect(util.transpileString(input)).toBe(lua); - } - - @TestCase("3+4", 3 + 4) - @TestCase("5-2", 5 - 2) - @TestCase("6*3", 6 * 3) - @TestCase("6**3", 6 ** 3) - @TestCase("20/5", 20 / 5) - @TestCase("15/10", 15 / 10) - @TestCase("15%3", 15 % 3) - @Test("Binary expressions basic numeric") - public binaryNum(input: string, output: number): void { - const result = util.transpileAndExecute(`return ${input}`); - - // Assert - Expect(result).toBe(output); - } - - @TestCase("1==1", true) - @TestCase("1===1", true) - @TestCase("1!=1", false) - @TestCase("1!==1", false) - @TestCase("1>1", false) - @TestCase("1>=1", true) - @TestCase("1<1", false) - @TestCase("1<=1", true) - @TestCase("1&&1", 1) - @TestCase("1||1", 1) - @Test("Binary expressions basic boolean") - public binaryBool(input: string, expected: any): void { - const result = util.transpileAndExecute(`return ${input}`); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase("'key' in obj") - @TestCase("'existingKey' in obj") - @TestCase("0 in obj") - @TestCase("9 in obj") - @Test("Binary expression in") - public binaryIn(input: string): void - { - const tsHeader = "declare var obj: any;"; - const tsSource = `return ${input}`; - const luaHeader = "obj = { existingKey = 1 }"; - const result = util.transpileAndExecute(tsSource, undefined, luaHeader, tsHeader); - - // Assert - Expect(result).toBe(eval(`let obj = { existingKey: 1 }; ${input}`)); - } - - @TestCase("a+=b", 5 + 3) - @TestCase("a-=b", 5 - 3) - @TestCase("a*=b", 5 * 3) - @TestCase("a/=b", 5 / 3) - @TestCase("a%=b", 5 % 3) - @TestCase("a**=b", 5 ** 3) - @Test("Binary expressions overridden operators") - public binaryOperatorOverride(input: string, expected: number): void { - const result = util.transpileAndExecute(`let a = 5; let b = 3; ${input}; return a;`); - - Expect(result).toBe(expected); - } - - @TestCase("~b") - @TestCase("a&b") - @TestCase("a&=b") - @TestCase("a|b") - @TestCase("a|=b") - @TestCase("a^b") - @TestCase("a^=b") - @TestCase("a<>b") - @TestCase("a>>=b") - @TestCase("a>>>b") - @TestCase("a>>>=b") - @Test("Bitop [5.1]") - public bitOperatorOverride51(input: string, lua: string): void { - // Bit operations not supported in 5.1, expect an exception - Expect(() => util.transpileString(input, { luaTarget: LuaTarget.Lua51, luaLibImport: LuaLibImportKind.None })) - .toThrow(); - } - - @TestCase("~a", "bit.bnot(a);") - @TestCase("a&b", "bit.band(a, b);") - @TestCase("a&=b", "a = bit.band(a, b);") - @TestCase("a|b", "bit.bor(a, b);") - @TestCase("a|=b", "a = bit.bor(a, b);") - @TestCase("a^b", "bit.bxor(a, b);") - @TestCase("a^=b", "a = bit.bxor(a, b);") - @TestCase("a<>b", "bit.rshift(a, b);") - @TestCase("a>>=b", "a = bit.rshift(a, b);") - @TestCase("a>>>b", "bit.arshift(a, b);") - @TestCase("a>>>=b", "a = bit.arshift(a, b);") - @Test("Bitop [JIT]") - public bitOperatorOverrideJIT(input: string, lua: string): void { - const options = { luaTarget: LuaTarget.LuaJIT, luaLibImport: LuaLibImportKind.None }; - Expect(util.transpileString(input, options)).toBe(lua); - } - - @TestCase("~a", "bit32.bnot(a);") - @TestCase("a&b", "bit32.band(a, b);") - @TestCase("a&=b", "a = bit32.band(a, b);") - @TestCase("a|b", "bit32.bor(a, b);") - @TestCase("a|=b", "a = bit32.bor(a, b);") - @TestCase("a^b", "bit32.bxor(a, b);") - @TestCase("a^=b", "a = bit32.bxor(a, b);") - @TestCase("a<>b", "bit32.rshift(a, b);") - @TestCase("a>>=b", "a = bit32.rshift(a, b);") - @TestCase("a>>>b", "bit32.arshift(a, b);") - @TestCase("a>>>=b", "a = bit32.arshift(a, b);") - @Test("Bitop [5.2]") - public bitOperatorOverride52(input: string, lua: string): void { - const options = { luaTarget: LuaTarget.Lua52, luaLibImport: LuaLibImportKind.None }; - Expect(util.transpileString(input, options)).toBe(lua); - } - - @TestCase("~a", "~a;") - @TestCase("a&b", "a & b;") - @TestCase("a&=b", "a = a & b;") - @TestCase("a|b", "a | b;") - @TestCase("a|=b", "a = a | b;") - @TestCase("a^b", "a ~ b;") - @TestCase("a^=b", "a = a ~ b;") - @TestCase("a<>b", "a >> b;") - @TestCase("a>>=b", "a = a >> b;") - @Test("Bitop [5.3]") - public bitOperatorOverride53(input: string, lua: string): void { - const options = { luaTarget: LuaTarget.Lua53, luaLibImport: LuaLibImportKind.None }; - Expect(util.transpileString(input, options)).toBe(lua); - } - - @TestCase("a>>>b") - @TestCase("a>>>=b") - @Test("Unsupported bitop 5.3") - public bitOperatorOverride53Unsupported(input: string): void { - Expect(() => util.transpileString(input, { luaTarget: LuaTarget.Lua53, luaLibImport: LuaLibImportKind.None })) - .toThrowError(TranspileError, "Bitwise >>> operator is/are not supported for target Lua 5.3."); - } - - @TestCase("1+1", "1 + 1;") - @TestCase("-1+1", "(-1) + 1;") - @TestCase("1*30+4", "(1 * 30) + 4;") - @TestCase("1*(3+4)", "1 * (3 + 4);") - @TestCase("1*(3+4*2)", "1 * (3 + (4 * 2));") - @Test("Binary expressions ordering parentheses") - public binaryParentheses(input: string, lua: string): void { - Expect(util.transpileString(input)).toBe(lua); - } - - @TestCase("bar(),foo()", 1) - @TestCase("foo(),bar()", 2) - @TestCase("foo(),bar(),baz()", 3) - @Test("Binary Comma") - public binaryComma(input: string, expectResult: number): void { - const code = - `function foo() { return 1; } - function bar() { return 2; }; - function baz() { return 3; }; - return (${input});`; - Expect(util.transpileAndExecute(code)).toBe(expectResult); - } - - @Test("Binary Comma Statement in For Loop") - public binaryCommaStatementInForLoop(): void { - const code = - `let x: number, y: number; - for (x = 0, y = 17; x < 5; ++x, --y) {} - return y;`; - Expect(util.transpileAndExecute(code)).toBe(12); - } - - @Test("Null Expression") - public nullExpression(): void { - Expect(util.transpileString("null")).toBe("nil;"); - } - - @Test("Undefined Expression") - public undefinedExpression(): void { - Expect(util.transpileString("undefined")).toBe("nil;"); - } - - @TestCase("true ? 'a' : 'b'", "a") - @TestCase("false ? 'a' : 'b'", "b") - @TestCase("true ? false : true", false) - @TestCase("false ? false : true", true) - @TestCase("true ? literalValue : true", "literal") - @TestCase("true ? variableValue : true", undefined) - @TestCase("true ? maybeUndefinedValue : true", undefined) - @TestCase("true ? maybeBooleanValue : true", false) - @TestCase("true ? maybeUndefinedValue : true", undefined, { strictNullChecks: true }) - @TestCase("true ? maybeBooleanValue : true", false, { strictNullChecks: true }) - @TestCase("true ? undefined : true", undefined, { strictNullChecks: true }) - @TestCase("true ? null : true", undefined, { strictNullChecks: true }) - @TestCase("true ? false : true", false, { luaTarget: LuaTarget.Lua51 }) - @TestCase("false ? false : true", true, { luaTarget: LuaTarget.Lua51 }) - @TestCase("true ? undefined : true", undefined, { luaTarget: LuaTarget.Lua51 }) - @TestCase("true ? false : true", false, { luaTarget: LuaTarget.LuaJIT }) - @TestCase("false ? false : true", true, { luaTarget: LuaTarget.LuaJIT }) - @TestCase("true ? undefined : true", undefined, { luaTarget: LuaTarget.LuaJIT }) - @Test("Ternary operator") - public ternaryOperator(input: string, expected: any, options?: ts.CompilerOptions): void { - const result = util.transpileAndExecute( - `const literalValue = 'literal'; - let variableValue:string; - let maybeBooleanValue:string|boolean = false; - let maybeUndefinedValue:string|undefined; - return ${input};`, options); - - Expect(result).toBe(expected); - } - - @TestCase("inst.field", 8) - @TestCase("inst.field + 3", 8 + 3) - @TestCase("inst.field * 3", 8 * 3) - @TestCase("inst.field / 2", 8 / 2) - @TestCase("inst.field && 3", 8 && 3) - @TestCase("inst.field || 3", 8 || 3) - @TestCase("(inst.field + 3) & 3", (8 + 3) & 3) - @TestCase("inst.field | 3", 8 | 3) - @TestCase("inst.field << 3", 8 << 3) - @TestCase("inst.field >> 1", 8 >> 1) - @TestCase("inst.field = 3", 3) - @TestCase(`"abc" + inst.field`, "abc8") - @Test("Get accessor expression") - public getAccessorBinary(expression: string, expected: any): void { - const source = `class MyClass {` - + ` public _field: number;` - + ` public get field(): number { return this._field + 4; }` - + ` public set field(v: number) { this._field = v; }` - + `}` - + `var inst = new MyClass();` - + `inst._field = 4;` - + `return ${expression};`; - - // Transpile/Execute - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase("= 4", 4 + 4) - @TestCase("-= 3", (4 - 3) + 4) - @TestCase("+= 3", (4 + 3) + 4) - @TestCase("*= 3", (4 * 3) + 4) - @TestCase("/= 2", (4 / 2) + 4) - @TestCase("&= 3", (4 & 3) + 4) - @TestCase("|= 3", (4 | 3) + 4) - @TestCase("<<= 3", (4 << 3) + 4) - @TestCase(">>= 3", (4 >> 3) + 4) - @Test("Set accessorExpression") - public setAccessorBinary(expression: string, expected: any): void { - const source = `class MyClass {` - + ` public _field: number = 4;` - + ` public get field(): number { return this._field; }` - + ` public set field(v: number) { this._field = v + 4; }` - + `}` - + `var inst = new MyClass();` - + `inst.field ${expression};` - + `return inst._field;`; - - // Transpile/Execute - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase("inst.baseField", 7) - @TestCase("inst.field", 6) - @TestCase("inst.superField", 5) - @TestCase("inst.superBaseField", 4) - @Test("Inherited accessors") - public inheritedAccessors(expression: string, expected: any): void { - const source = `class MyBaseClass {` - + ` public _baseField: number;` - + ` public get baseField(): number { return this._baseField + 6; }` - + ` public set baseField(v: number) { this._baseField = v; }` - + `}` - + `class MyClass extends MyBaseClass {` - + ` public _field: number;` - + ` public get field(): number { return this._field + 4; }` - + ` public set field(v: number) { this._field = v; }` - + `}` - + `class MySuperClass extends MyClass {` - + ` public _superField: number;` - + ` public get superField(): number { return this._superField + 2; }` - + ` public set superField(v: number) { this._superField = v; }` - + ` public get superBaseField() { return this.baseField - 3; }` - + `}` - + `var inst = new MySuperClass();` - + `inst.baseField = 1;` - + `inst.field = 2;` - + `inst.superField = 3;` - + `return ${expression};`; - - const result = util.transpileAndExecute(source); - Expect(result).toBe(expected); - } - - @TestCase("return x.value;", 1) - @TestCase("x.value = 3; return x.value;", 3) - @Test("Union accessors") - public unionAccessors(expression: string, expected: any): void { - const result = util.transpileAndExecute( - `class A{ get value(){ return this.v || 1; } set value(v){ this.v = v; } v: number; } - class B{ get value(){ return this.v || 2; } set value(v){ this.v = v; } v: number; } - let x: A|B = new A(); - ${expression}` - ); - - Expect(result).toBe(expected); - } - - @TestCase("i++", 10) - @TestCase("i--", 10) - @TestCase("++i", 11) - @TestCase("--i", 9) - @Test("Incrementor value") - public incrementorValue(expression: string, expected: number): void { - const result = util.transpileAndExecute(`let i = 10; return ${expression};`); - - Expect(result).toBe(expected); - } - - @TestCase("a++", "val3") - @TestCase("a--", "val3") - @TestCase("--a", "val2") - @TestCase("++a", "val4") - @Test("Template string expression") - public templateStringExpression(lambda: string, expected: string): void { - const result = util.transpileAndExecute("let a = 3; return `val${" + lambda + "}`;"); - - Expect(result).toEqual(expected); - } - - @TestCase("x = y", "y") - @TestCase("x += y", "xy") - @Test("Assignment expressions") - public assignmentExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute(`let x = "x"; let y = "y"; return ${expression};`); - Expect(result).toBe(expected); - } - - @TestCase("x = o.p", "o") - @TestCase("x = a[0]", "a") - @TestCase("x = y = o.p", "o") - @TestCase("x = o.p", "o") - @Test("Assignment expressions using temp") - public assignmentWithTempExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute( - `let x = "x"; - let y = "y"; - let o = {p: "o"}; - let a = ["a"]; - return ${expression};`); - Expect(result).toBe(expected); - } - - @TestCase("o.p = x", "x") - @TestCase("a[0] = x", "x") - @TestCase("o.p = a[0]", "a") - @TestCase("o.p = a[0] = x", "x") - @Test("Property assignment expressions") - public propertyAssignmentExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute( - `let x = "x"; - let o = {p: "o"}; - let a = ["a"]; - return ${expression};`); - Expect(result).toBe(expected); - } - - @TestCase("x = t()", "t0,t1") - @TestCase("x = tr()", "tr0,tr1") - @TestCase("[x[1], x[0]] = t()", "t0,t1") - @TestCase("[x[1], x[0]] = tr()", "tr0,tr1") - @TestCase("x = [y[1], y[0]]", "y1,y0") - @TestCase("[x[0], x[1]] = [y[1], y[0]]", "y1,y0") - @Test("Tuple assignment expressions") - public tupleAssignmentExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute( - `let x: [string, string] = ["x0", "x1"]; - let y: [string, string] = ["y0", "y1"]; - function t(): [string, string] { return ["t0", "t1"] }; - /** @tupleReturn */ - function tr(): [string, string] { return ["tr0", "tr1"] }; - const r = ${expression}; - return \`\${r[0]},\${r[1]}\``); - Expect(result).toBe(expected); - } - - @Test("Block expression") - public blockExpresion(): void { - const result = util.transpileAndExecute(`let a = 4; {let a = 42; } return a;`); - Expect(result).toBe(4); - } - - @Test("Non-null expression") - public nonNullExpression(): void { - const result = util.transpileAndExecute(`function abc(): number | undefined { return 3; } - const a: number = abc()!; - return a;`); - Expect(result).toBe(3); - } - // ==================================== - // Test expected errors - // ==================================== - - @Test("Unknown unary postfix error") - public unknownUnaryPostfixError(): void { - const transformer = util.makeTestTransformer(); - - const mockExpression: any = { - operand: ts.createLiteral(false), - operator: ts.SyntaxKind.AsteriskToken, - }; - - Expect(() => transformer.transformPostfixUnaryExpression(mockExpression as ts.PostfixUnaryExpression)) - .toThrowError(TranspileError, "Unsupported unary postfix operator kind: AsteriskToken"); - } - - @Test("Unknown unary postfix error") - public unknownUnaryPrefixError(): void { - const transformer = util.makeTestTransformer(); - - const mockExpression: any = { - operand: ts.createLiteral(false), - operator: ts.SyntaxKind.AsteriskToken, - }; - - Expect(() => transformer.transformPrefixUnaryExpression(mockExpression as ts.PrefixUnaryExpression)) - .toThrowError(TranspileError, "Unsupported unary prefix operator kind: AsteriskToken"); - } - - @Test("Incompatible fromCodePoint expression error") - public incompatibleFromCodePointExpression(): void { - const transformer = util.makeTestTransformer(LuaTarget.LuaJIT); - - const identifier = ts.createIdentifier("fromCodePoint"); - Expect(() => transformer.transformStringExpression(identifier)) - .toThrowError(TranspileError, "string property fromCodePoint is/are not supported " + - "for target Lua jit."); - } - - @Test("Unknown string expression error") - public unknownStringExpression(): void { - const transformer = util.makeTestTransformer(LuaTarget.LuaJIT); - - const identifier = ts.createIdentifier("abcd"); - Expect(() => transformer.transformStringExpression(identifier)) - .toThrowError(TranspileError, "string property abcd is/are not supported for target Lua jit."); - } - - @Test("Unsupported array function error") - public unsupportedArrayFunctionError(): void { - const transformer = util.makeTestTransformer(); - - const mockNode: any = { - arguments: [], - caller: ts.createLiteral(false), - expression: {name: ts.createIdentifier("unknownFunction"), expression: ts.createLiteral(false)}, - }; - - Expect(() => transformer.transformArrayCallExpression(mockNode as ts.CallExpression)) - .toThrowError(TranspileError, "Unsupported property on array: unknownFunction"); - } - - @Test("Unsupported math property error") - public unsupportedMathPropertyError(): void { - const transformer = util.makeTestTransformer(); - - Expect(() => transformer.transformMathExpression(ts.createIdentifier("unknownProperty"))) - .toThrowError(TranspileError, "Unsupported property on math: unknownProperty"); - } - - @Test("Unsupported object literal element error") - public unsupportedObjectLiteralElementError(): void { - const transformer = util.makeTestTransformer(); - - const mockObject: any = { - properties: [{ - kind: ts.SyntaxKind.FalseKeyword, - name: ts.createIdentifier("testProperty"), - }], - }; - - Expect(() => transformer.transformObjectLiteral(mockObject as ts.ObjectLiteralExpression)) - .toThrowError(TranspileError, "Unsupported object literal element kind: FalseKeyword"); - } +import * as tstl from "../../src"; +import { unsupportedForTarget, unsupportedRightShiftOperator } from "../../src/transformation/utils/diagnostics"; +import * as util from "../util"; + +test.each([ + "i++", + "++i", + "i--", + "--i", + "!a", + "-a", + "+a", + "let a = delete tbl['test']", + "delete tbl['test']", + "let a = delete tbl.test", + "delete tbl.test", +])("Unary expressions basic (%p)", input => { + util.testFunction(input).disableSemanticCheck().expectLuaToMatchSnapshot(); +}); + +for (const expression of ["3+4", "5-2", "6*3", "6**3", "20/5", "15/10", "15%3"]) { + util.testEachVersion( + `Binary expressions basic numeric (${expression})`, + () => util.testExpression(expression), + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); } + +test.each(["1==1", "1===1", "1!=1", "1!==1", "1>1", "1>=1", "1<1", "1<=1", "1&&1", "1||1"])( + "Binary expressions basic boolean (%p)", + input => { + util.testExpression(input).expectToMatchJsResult(); + } +); + +test.each(["'key' in obj", "'existingKey' in obj", "0 in obj", "9 in obj"])("Binary expression in (%p)", input => { + util.testFunction` + let obj = { existingKey: 1 }; + return ${input}; + `.expectToMatchJsResult(); +}); + +test.each(["a+=b", "a-=b", "a*=b", "a/=b", "a%=b", "a**=b"])("Binary expressions overridden operators (%p)", input => { + util.testFunction` + let a = 5; + let b = 3; + ${input}; + return a; + `.expectToMatchJsResult(); +}); + +const supportedInAll = ["~a", "a&b", "a&=b", "a|b", "a|=b", "a^b", "a^=b", "a<>>b", "a>>>=b"]; +const unsupportedIn53And54 = ["a>>b", "a>>=b"]; +const allBinaryOperators = [...supportedInAll, ...unsupportedIn53And54]; +test.each(allBinaryOperators)("Bitop [5.0] (%p)", input => { + // Bit operations not supported in 5.0, expect an exception + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua50, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]); +}); + +test.each(allBinaryOperators)("Bitop [5.1] (%p)", input => { + // Bit operations not supported in 5.1, expect an exception + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua51, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]); +}); + +test.each(allBinaryOperators)("Bitop [JIT] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.LuaJIT, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(allBinaryOperators)("Bitop [5.2] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua52, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(supportedInAll)("Bitop [5.3] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(supportedInAll)("Bitop [5.4] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua54, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .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 }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); +}); + +test.each(unsupportedIn53And54)("Unsupported bitop 5.4 (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua54, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([unsupportedRightShiftOperator.code]); +}); + +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 => { + util.testExpression(input).expectLuaToMatchSnapshot(); + } +); + +test("Assignment order of operations is preserved", () => { + util.testFunction` + let x = 0; + x *= 2 + 3; + return x; + `.expectToMatchJsResult(); +}); + +test.each(["bar(),foo()", "foo(),bar()", "foo(),bar(),baz()"])("Binary Comma (%p)", input => { + util.testFunction` + function foo() { return 1; } + function bar() { return 2; }; + function baz() { return 3; }; + return (${input}); + `.expectToMatchJsResult(); +}); + +test("Binary Comma Statement in For Loop", () => { + util.testFunction` + let x: number, y: number; + for (x = 0, y = 17; x < 5; ++x, --y) {} + return y; + `.expectToMatchJsResult(); +}); + +test("Null Expression", () => { + util.testExpression("null").expectLuaToMatchSnapshot(); +}); + +test("Undefined Expression", () => { + util.testExpression("undefined").expectLuaToMatchSnapshot(); +}); + +test.each(["i++", "i--", "++i", "--i"])("Incrementor value (%p)", expression => { + util.testFunction` + let i = 10; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test("Non-null expression", () => { + util.testFunction` + function abc(): number | undefined { return 3; } + const a: number = abc()!; + return a; + `.expectToMatchJsResult(); +}); + +test("Typescript 4.7 instantiation expression", () => { + util.testFunction` + function foo(x: T): T { return x; } + const bar = foo; + return bar(3); + `.expectToMatchJsResult(); +}); + +test("Typescript 4.9 satisfies expression", () => { + util.testFunction` + const foo = { a: 1 } satisfies { a: number }; + return foo.a; + `.expectToMatchJsResult(); +}); + +test.each([ + '"foobar"', + "17", + "true", + "{}", + "[]", + "[].length", + "foo() + foo()", + "!foo()", + "foo()", + "typeof foo", + '"foo" in bar', + "foo as Function", + "Math.log2(2)", + "Math.log10(2)", + '"".indexOf("")', +])("Expression statements (%p)", input => { + util.testFunction` + function foo() { return 17; } + const bar = { foo }; + ${input}; + `.expectNoExecutionError(); +}); diff --git a/test/unit/file.spec.ts b/test/unit/file.spec.ts new file mode 100644 index 000000000..12a136408 --- /dev/null +++ b/test/unit/file.spec.ts @@ -0,0 +1,41 @@ +import * as util from "../util"; + +describe("JSON", () => { + test.each([0, "", [], [1, "2", []], { a: "b" }, { a: { b: "c" } }])("JSON (%p)", json => { + util.testModule(JSON.stringify(json)).setMainFileName("main.json").expectToEqual(json); + }); + + test("empty file throws runtime error", () => { + util.testModule("") + .setMainFileName("main.json") + .expectToEqual(new util.ExecutionError("Unexpected end of JSON input")); + }); + + test("JSON modules can be imported", () => { + util.testModule` + import * as jsonData from "./jsonModule.json"; + export const result = jsonData; + ` + .addExtraFile("jsonModule.json", '{ "jsonField1": "hello, this is JSON", "jsonField2": ["a", "b", "c"] }') + .expectToEqual({ + result: { + jsonField1: "hello, this is JSON", + jsonField2: ["a", "b", "c"], + }, + }); + }); +}); + +describe("shebang", () => { + test("LF", () => { + util.testModule`#!/usr/bin/env lua\n + const foo = true; + `.expectLuaToMatchSnapshot(); + }); + + test("CRLF", () => { + util.testModule`#!/usr/bin/env lua\r\n + const foo = true; + `.expectLuaToMatchSnapshot(); + }); +}); diff --git a/test/unit/find-lua-requires.spec.ts b/test/unit/find-lua-requires.spec.ts new file mode 100644 index 000000000..bb879d642 --- /dev/null +++ b/test/unit/find-lua-requires.spec.ts @@ -0,0 +1,159 @@ +import { findLuaRequires, LuaRequire } from "../../src/transpilation/find-lua-requires"; + +test("empty string", () => { + expect(findLuaRequires("")).toEqual([]); +}); + +test("can find requires", () => { + const lua = ` + require("req1") + require('req2') + local r3 = require("req3") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", "req2", "req3"]); +}); + +test("handles requires with spacing", () => { + const lua = 'require \t ( \t "req" )'; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req"]); +}); + +test("handles requires without parentheses", () => { + const lua = 'require "req"'; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req"]); +}); + +test("has correct offsets", () => { + const lua = ` + require("req1") + require('req2') + local r3 = require("req3") + `; + const requires = findLuaRequires(lua); + expect(requires).toHaveLength(3); + expect(lua.substring(requires[0].from, requires[0].to + 1)).toBe('require("req1")'); + expect(lua.substring(requires[1].from, requires[1].to + 1)).toBe("require('req2')"); + expect(lua.substring(requires[2].from, requires[2].to + 1)).toBe('require("req3")'); +}); + +test("has correct offsets for offsets without parentheses", () => { + const lua = ` + require"req1" + require 'req2' + local r3 = require"req3" + `; + const requires = findLuaRequires(lua); + expect(requires).toHaveLength(3); + expect(lua.substring(requires[0].from, requires[0].to + 1)).toBe('require"req1"'); + expect(lua.substring(requires[1].from, requires[1].to + 1)).toBe("require 'req2'"); + expect(lua.substring(requires[2].from, requires[2].to + 1)).toBe('require"req3"'); +}); + +test("ignores requires that should not be included", () => { + const lua = ` + require("req1") + local a = "require('This should not be included')" + require('req2') + local b = 'require("This should not be included")' + require("req3") + -- require("this should not be included") + require("req4") + --[[ require("this should not be included") ]] + require("req5") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", "req2", "req3", "req4", "req5"]); +}); + +test("non-terminated require", () => { + expect(findLuaRequires("require('abc")).toEqual([]); +}); + +describe.each(['"', "'"])("strings with delimiter %p", delimiter => { + test("escaped delimiter", () => { + const lua = ` + require(${delimiter}req1${delimiter}); + local a = ${delimiter}require(excludeThis\\${delimiter})${delimiter} + require(${delimiter}req\\${delimiter}2${delimiter}); + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", `req${delimiter}2`]); + }); + + test("multiple escaped delimiters", () => { + const lua = ` + require(${delimiter}r\\${delimiter}e.- q%\\${delimiter}1${delimiter}) + `; + expect(requirePaths(findLuaRequires(lua))).toEqual([`r${delimiter}e.- q%${delimiter}1`]); + }); + + test("handles other escaped characters", () => { + expect(requirePaths(findLuaRequires(`require(${delimiter}req\\n\\\\${delimiter})`))).toEqual(["req\\n\\\\"]); + }); + + test("handles non-delimiter quote", () => { + const oppositeDelimiter = delimiter === "'" ? '"' : "'"; + const lua = ` + require(${delimiter}req1${delimiter}); + local a = ${delimiter}require(excludeThis${oppositeDelimiter})${delimiter}; + require(${delimiter}req2${oppositeDelimiter}${delimiter}); + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", `req2${oppositeDelimiter}`]); + }); + + test("non-terminated string", () => { + const lua = ` + require(${delimiter}myRequire${delimiter}); + local a = ${delimiter}require("excludeThis") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["myRequire"]); + }); +}); + +describe("single-line comments", () => { + test("comment at end of file", () => { + expect(findLuaRequires("--")).toEqual([]); + }); + + test("require before and after comment", () => { + const lua = ` + require("req1")-- comment\nrequire("req2") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", "req2"]); + }); + + test("require before and after empty comment", () => { + const lua = ` + require("req1")--\nrequire("req2") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", "req2"]); + }); +}); + +describe("multi-line comments", () => { + test("comment at end of file", () => { + expect(findLuaRequires("--[[]]")).toEqual([]); + }); + + test("unterminated comment", () => { + expect(findLuaRequires("--[[")).toEqual([]); + }); + + test("require before and after comment", () => { + const lua = ` + require("req1")--[[ + ml comment require("this should be excluded") + ]]require("req2") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", "req2"]); + }); + + test("require before and after empty comment", () => { + const lua = ` + require("req1")--[[]]require("req2") + `; + expect(requirePaths(findLuaRequires(lua))).toEqual(["req1", "req2"]); + }); +}); + +function requirePaths(matches: LuaRequire[]): string[] { + return matches.map(m => m.requirePath); +} diff --git a/test/unit/functions.spec.ts b/test/unit/functions.spec.ts deleted file mode 100644 index 71b1b96b7..000000000 --- a/test/unit/functions.spec.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import * as ts from "typescript"; -import * as util from "../src/util"; - -export class FunctionTests { - - @Test("Arrow Function Expression") - public arrowFunctionExpression(): void - { - const result = util.transpileAndExecute(`let add = (a, b) => a+b; return add(1,2);`); - - // Assert - Expect(result).toBe(3); - } - - @TestCase("i++", 15) - @TestCase("i--", 5) - @TestCase("++i", 15) - @TestCase("--i", 5) - @Test("Arrow function unary expression") - public arrowFunctionUnary(lambda: string, expected: number): void { - const result = util.transpileAndExecute(`let i = 10; [1,2,3,4,5].forEach(() => ${lambda}); return i;`); - - Expect(result).toBe(expected); - } - - @TestCase("b => a = b", 5) - @TestCase("b => a += b", 15) - @TestCase("b => a -= b", 5) - @TestCase("b => a *= b", 50) - @TestCase("b => a /= b", 2) - @TestCase("b => a **= b", 100000) - @TestCase("b => a %= b", 0) - @Test("Arrow function assignment") - public arrowFunctionAssignment(lambda: string, expected: number): void - { - const result = util.transpileAndExecute(`let a = 10; let lambda = ${lambda}; - lambda(5); return a;`); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase([]) - @TestCase([5]) - @TestCase([1, 2]) - @Test("Arrow Default Values") - public arrowExpressionDefaultValues(inp: number[]): void { - // Default value is 3 for v1 - const v1 = inp.length > 0 ? inp[0] : 3; - // Default value is 4 for v2 - const v2 = inp.length > 1 ? inp[1] : 4; - - const callArgs = inp.join(","); - - // Transpile/Execute - const result = util.transpileAndExecute( - `let add = (a: number = 3, b: number = 4) => a+b; - return add(${callArgs});` - ); - - // Assert - Expect(result).toBe(v1 + v2); - } - - @Test("Function Expression") - public functionExpression(): void - { - const result = util.transpileAndExecute(`let add = function(a, b) {return a+b}; return add(1,2);`); - - // Assert - Expect(result).toBe(3); - } - - @Test("Function definition scope") - public functionDefinitionScope(): void - { - const result = util.transpileAndExecute(`function abc() { function xyz() { return 5; } }\n - function def() { function xyz() { return 3; } abc(); return xyz(); }\n - return def();`); - - // Assert - Expect(result).toBe(3); - } - - @Test("Function default parameter") - public functionDefaultParameter(): void - { - const result = util.transpileAndExecute(`function abc(defaultParam: string = "abc") { return defaultParam; }\n - return abc() + abc("def");`); - - // Assert - Expect(result).toBe("abcdef"); - } - - @TestCase([]) - @TestCase([5]) - @TestCase([1, 2]) - @Test("Function Default Values") - public functionExpressionDefaultValues(inp: number[]): void { - // Default value is 3 for v1 - const v1 = inp.length > 0 ? inp[0] : 3; - // Default value is 4 for v2 - const v2 = inp.length > 1 ? inp[1] : 4; - - const callArgs = inp.join(","); - - // Transpile/Execute - const result = util.transpileAndExecute( - `let add = function(a: number = 3, b: number = 4) { return a+b; }; - return add(${callArgs});` - ); - - // Assert - Expect(result).toBe(v1 + v2); - } - - @Test("Class method call") - public classMethod(): void { - const returnValue = 4; - const source = `class TestClass { - public classMethod(): number { return ${returnValue}; } - } - - const classInstance = new TestClass(); - return classInstance.classMethod();`; - - // Transpile/Execute - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(returnValue); - } - - @Test("Class dot method call void") - public classDotMethod(): void { - const returnValue = 4; - const source = `class TestClass { - public dotMethod: () => number = () => ${returnValue}; - } - - const classInstance = new TestClass(); - return classInstance.dotMethod();`; - - // Transpile/Execute - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(returnValue); - } - - @Test("Class dot method call with parameter") - public classDotMethod2(): void { - const returnValue = 4; - const source = `class TestClass { - public dotMethod: (x: number) => number = x => 3 * x; - } - - const classInstance = new TestClass(); - return classInstance.dotMethod(${returnValue});`; - - // Transpile - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(3 * returnValue); - } - - @Test("Class static dot method") - public classDotMethodStatic(): void { - const returnValue = 4; - const source = `class TestClass { - public static dotMethod: () => number = () => ${returnValue}; - } - - return TestClass.dotMethod();`; - - // Transpile/Execute - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(returnValue); - } - - @Test("Class static dot method with parameter") - public classDotMethodStaticWithParameter(): void { - const returnValue = 4; - const source = `class TestClass { - public static dotMethod: (x: number) => number = x => 3 * x; - } - - return TestClass.dotMethod(${returnValue});`; - - // Transpile - const result = util.transpileAndExecute(source); - - // Assert - Expect(result).toBe(3 * returnValue); - } - - @Test("Function bind") - public functionBind(): void { - const source = `const abc = function (this: { a: number }, a: string, b: string) { return this.a + a + b; } - return abc.bind({ a: 4 }, "b")("c");`; - - const result = util.transpileAndExecute(source); - - Expect(result).toBe("4bc"); - } - - @Test("Function apply") - public functionApply(): void { - const source = `const abc = function (this: { a: number }, a: string) { return this.a + a; } - return abc.apply({ a: 4 }, ["b"]);`; - - const result = util.transpileAndExecute(source); - - Expect(result).toBe("4b"); - } - - @Test("Function call") - public functionCall(): void { - const source = `const abc = function (this: { a: number }, a: string) { return this.a + a; } - return abc.call({ a: 4 }, "b");`; - - const result = util.transpileAndExecute(source); - - Expect(result).toBe("4b"); - } - - @Test("Invalid property access call transpilation") - public invalidPropertyCall(): void { - const transformer = util.makeTestTransformer(); - - const mockObject: any = { - expression: ts.createLiteral("abc"), - }; - - Expect(() => transformer.transformPropertyCall(mockObject as ts.CallExpression)) - .toThrowError(Error, "Tried to transpile a non-property call as property call."); - } - - @Test("Function dead code after return") - public functionDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `function abc() { return 3; const a = 5; } return abc();`); - - Expect(result).toBe(3); - } - - @Test("Method dead code after return") - public methodDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `class def { public static abc() { return 3; const a = 5; } } return def.abc();`); - - Expect(result).toBe(3); - } - - @Test("Recursive function definition") - public recursiveFunctionDefinition(): void { - const result = util.transpileAndExecute( - `function f() { return typeof f; }; return f();`); - - Expect(result).toBe("function"); - } - - @Test("Recursive function expression") - public recursiveFunctionExpression(): void { - const result = util.transpileAndExecute( - `let f = function() { return typeof f; }; return f();`); - - Expect(result).toBe("function"); - } - - @Test("Wrapped recursive function expression") - public wrappedRecursiveFunctionExpression(): void { - const result = util.transpileAndExecute( - `function wrap(fn: T) { return fn; } - let f = wrap(function() { return typeof f; }); return f();`); - - Expect(result).toBe("function"); - } - - @Test("Recursive arrow function") - public recursiveArrowFunction(): void { - const result = util.transpileAndExecute( - `let f = () => typeof f; return f();`); - - Expect(result).toBe("function"); - } - - @Test("Wrapped recursive arrow function") - public wrappedRecursiveArrowFunction(): void { - const result = util.transpileAndExecute( - `function wrap(fn: T) { return fn; } - let f = wrap(() => typeof f); return f();`); - - Expect(result).toBe("function"); - } - - @Test("Object method declaration") - public objectMethodDeclaration(): void { - const result = util.transpileAndExecute( - `let o = { v: 4, m(i: number): number { return this.v * i; } }; return o.m(3);`); - Expect(result).toBe(12); - } - - @TestCase(["bar"], "foobar") - @TestCase(["baz", "bar"], "bazbar") - @Test("Function overload") - public functionOverload(args: string[], expectResult: string): void { - const code = `class O { - prop = "foo"; - method(s: string): string; - method(this: void, s1: string, s2: string): string; - method(s1: string) { - if (typeof this === "string") { - return this + s1; - } - return this.prop + s1; - } - }; - const o = new O(); - return o.method(${args.map(a => "\"" + a + "\"").join(", ")});`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } - - @Test("Nested Function") - public nestedFunction(): void { - const code = `class C { - private prop = "bar"; - public outer() { - const o = { - prop: "foo", - innerFunc: function() { return this.prop; }, - innerArrow: () => this.prop - }; - return o.innerFunc() + o.innerArrow(); - } - } - let c = new C(); - return c.outer();`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foobar"); - } - - @TestCase("abc", "abc") - @TestCase("abc", "def") - @Test("Dot vs Colon method call") - public dotVColonMethodCall(s1: string, s2: string): void - { - const result = util.transpileAndExecute( - `class MyClass { - dotMethod(this: void, s: string) { - return s; - } - colonMethod(s: string) { - return s; - } - } - const inst = new MyClass(); - return inst.dotMethod("${s1}") == inst.colonMethod("${s2}");` - ); - Expect(result).toBe(s1 === s2); - } - - @Test("Element access call") - public elementAccessCall(): void { - const code = `class C { - prop = "bar"; - method(s: string) { return s + this.prop; } - } - const c = new C(); - return c['method']("foo"); - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foobar"); - } - - @Test("Element access call no args") - public elementAccessCallNoArgs(): void { - const code = `class C { - prop = "bar"; - method() { return this.prop; } - } - const c = new C(); - return c['method'](); - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe("bar"); - } - - @Test("Complex element access call") - public elementAccessCallComplex(): void { - const code = `class C { - prop = "bar"; - method(s: string) { return s + this.prop; } - } - function getC() { return new C(); } - return getC()['method']("foo"); - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foobar"); - } - - @Test("Complex element access call no args") - public elementAccessCallComplexNoArgs(): void { - const code = `class C { - prop = "bar"; - method() { return this.prop; } - } - function getC() { return new C(); } - return getC()['method'](); - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe("bar"); - } - - @Test("Complex element access call statement") - public elementAccessCallComplexStatement(): void { - const code = `let foo: string; - class C { - prop = "bar"; - method(s: string) { foo = s + this.prop; } - } - function getC() { return new C(); } - getC()['method']("foo"); - return foo; - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foobar"); - } - - @TestCase(1, 1) - @TestCase(2, 42) - @Test("Generator functions value") - public generatorFunctionValue(iterations: number, expectedResult: number): void { - const code = `function* seq(value: number) { - let a = yield value + 1; - return 42; - } - const gen = seq(0); - let ret: number; - for(let i = 0; i < ${iterations}; ++i) - { - ret = gen.next(i).value; - } - return ret; - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectedResult); - } - - @TestCase(1, false) - @TestCase(2, true) - @Test("Generator functions done") - public generatorFunctionDone(iterations: number, expectedResult: boolean): void { - const code = `function* seq(value: number) { - let a = yield value + 1; - return 42; - } - const gen = seq(0); - let ret: boolean; - for(let i = 0; i < ${iterations}; ++i) - { - ret = gen.next(i).done; - } - return ret; - `; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectedResult); - } - - @Test("Generator for..of") - public generatorFunctionForOf(): void { - const code = `function* seq() { - yield(1); - yield(2); - yield(3); - return 4; - } - let result = 0; - for(let i of seq()) - { - result = result * 10 + i; - } - return result`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(123); - } - - @Test("Function local overriding export") - public functionLocalOverridingExport(): void { - const code = - `export const foo = 5; - function bar(foo: number) { - return foo; - } - export const result = bar(7);`; - Expect(util.transpileExecuteAndReturnExport(code, "result")).toBe(7); - } - - @Test("Function using global as this") - public functionUsingGlobalAsThis(): void { - const code = - `var foo = "foo"; - function bar(this: any) { - return this.foo; - }`; - Expect(util.transpileAndExecute("return foo;", undefined, undefined, code)).toBe("foo"); - } -} diff --git a/test/unit/functions/__snapshots__/functions.spec.ts.snap b/test/unit/functions/__snapshots__/functions.spec.ts.snap new file mode 100644 index 000000000..83345e13b --- /dev/null +++ b/test/unit/functions/__snapshots__/functions.spec.ts.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Function default parameter with value "null" 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function foo(self, x) + return x + end + return foo(nil) +end +return ____exports" +`; + +exports[`Function default parameter with value "undefined" 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function foo(self, x) + return x + end + return foo(nil) +end +return ____exports" +`; + +exports[`function.length unsupported ("5.0"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function fn(self) + end + return debug.getinfo(fn).nparams - 1 +end +return ____exports" +`; + +exports[`function.length unsupported ("5.0"): diagnostics 1`] = `"main.ts(3,20): error TSTL: function.length is/are not supported for target Lua 5.0."`; + +exports[`function.length unsupported ("5.1"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function fn(self) + end + return debug.getinfo(fn).nparams - 1 +end +return ____exports" +`; + +exports[`function.length unsupported ("5.1"): diagnostics 1`] = `"main.ts(3,20): error TSTL: function.length is/are not supported for target Lua 5.1."`; + +exports[`function.length unsupported ("universal"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function fn(self) + end + return debug.getinfo(fn).nparams - 1 +end +return ____exports" +`; + +exports[`function.length unsupported ("universal"): diagnostics 1`] = `"main.ts(3,20): error TSTL: function.length is/are not supported for target Lua universal."`; + +exports[`missing declaration name: code 1`] = ` +"function ____(self) +end" +`; + +exports[`missing declaration name: diagnostics 1`] = `"main.ts(2,18): error TS1003: Identifier expected."`; diff --git a/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap new file mode 100644 index 000000000..64eb65457 --- /dev/null +++ b/test/unit/functions/__snapshots__/noSelfAnnotation.spec.ts.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@noSelf on declared function removes context argument 1`] = `"myFunction()"`; + +exports[`@noSelf on method inside class declaration removes context argument 1`] = `"holder.myMethod()"`; + +exports[`@noSelf on method inside interface declaration removes context argument 1`] = `"holder.myMethod()"`; + +exports[`@noSelf on method inside namespace declaration removes context argument 1`] = `"MyNamespace.myMethod()"`; + +exports[`@noSelf on parent class declaration removes context argument 1`] = `"holder.myMethod()"`; + +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 new file mode 100644 index 000000000..16b61ab46 --- /dev/null +++ b/test/unit/functions/functionProperties.spec.ts @@ -0,0 +1,202 @@ +import * as util from "../../util"; + +test("property on function", () => { + util.testFunction` + function foo(s: string) { return s; } + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("property on void function", () => { + util.testFunction` + function foo(this: void, s: string) { return s; } + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("property on recursively referenced function", () => { + util.testFunction` + function foo(s: string) { return s + foo.bar; } + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("property on hoisted function", () => { + util.testFunction` + foo.bar = "bar"; + function foo(s: string) { return s; } + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("function merged with namespace", () => { + util.testModule` + function foo(s: string) { return s; } + namespace foo { + export let bar = "bar"; + } + export const result = foo("foo") + foo.bar; + ` + .setReturnExport("result") + .expectToEqual("foobar"); +}); + +test("function with property assigned to variable", () => { + util.testFunction` + const foo = function(s: string) { return s; }; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("void function with property assigned to variable", () => { + util.testFunction` + const foo = function(this: void, s: string) { return s; }; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("named function with property assigned to variable", () => { + util.testFunction` + const foo = function baz(s: string) { return s; } + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("recursively referenced function with property assigned to variable", () => { + util.testFunction` + const foo = function(s: string) { return s + foo.bar; }; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("named recursively referenced function with property assigned to variable", () => { + util.testFunction` + const foo = function baz(s: string) { return s + foo.bar + baz.bar; }; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("arrow function with property assigned to variable", () => { + util.testFunction` + const foo: { (s: string): string; bar: string; } = s => s; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("void arrow function with property assigned to variable", () => { + util.testFunction` + const foo: { (this: void, s: string): string; bar: string; } = s => s; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("recursively referenced arrow function with property assigned to variable", () => { + util.testFunction` + const foo: { (s: string): string; bar: string; } = s => s + foo.bar; + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); + +test("property on generator function", () => { + util.testFunction` + function *foo(s: string) { yield s; } + foo.bar = "bar"; + for (const s of foo("foo")) { + return s + foo.bar; + } + `.expectToMatchJsResult(); +}); + +test("generator function assigned to variable", () => { + util.testFunction` + const foo = function *(s: string) { yield s; } + foo.bar = "bar"; + for (const s of foo("foo")) { + return s + foo.bar; + } + `.expectToMatchJsResult(); +}); + +test("property on async function", () => { + util.testFunction` + let result = ""; + async function foo(s: string) { result = s + foo.bar; } + foo.bar = "bar"; + void foo("foo"); + return result; + `.expectToMatchJsResult(); +}); + +test("async function with property assigned to variable", () => { + util.testFunction` + let result = ""; + const foo = async function(s: string) { result = s + foo.bar; } + foo.bar = "bar"; + void foo("foo"); + return result; + `.expectToMatchJsResult(); +}); + +test("async arrow function with property assigned to variable", () => { + util.testFunction` + let result = ""; + const foo: { (s: string): Promise; bar: string; } = async s => { result = s + foo.bar; }; + foo.bar = "bar"; + void foo("foo"); + return result; + `.expectToMatchJsResult(); +}); + +test("call function with property using call method", () => { + util.testFunction` + function foo(s: string) { return this + s; } + foo.baz = "baz"; + return foo.call("foo", "bar") + foo.baz; + `.expectToMatchJsResult(); +}); + +test("call function with property using apply method", () => { + util.testFunction` + function foo(s: string) { return this + s; } + foo.baz = "baz"; + return foo.apply("foo", ["bar"]) + foo.baz; + `.expectToMatchJsResult(); +}); + +test("call function with property using bind method", () => { + util.testFunction` + function foo(s: string) { return this + s; } + foo.baz = "baz"; + return foo.bind("foo", "bar")() + foo.baz; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1196 +test("Does not wrap simple variable declaration", () => { + util.testFunction` + function foo(s: string) { return s; } + foo.bar = "bar"; + const foo2 = foo; + return foo2("foo") + foo2.bar; + `.expectToMatchJsResult(); +}); + +test("Wraps function in inner expression", () => { + util.testFunction` + type Foo = { (s: string): string; bar: string; } + const foo = (s => s)! as Foo + foo.bar = "bar"; + return foo("foo") + foo.bar; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts new file mode 100644 index 000000000..7bdf6b040 --- /dev/null +++ b/test/unit/functions/functions.spec.ts @@ -0,0 +1,572 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; +import * as util from "../../util"; +import { unsupportedForTarget } from "../../../src/transformation/utils/diagnostics"; + +test("Arrow Function Expression", () => { + util.testFunction` + const add = (a, b) => a + b; + return add(1, 2); + `.expectToMatchJsResult(); +}); + +test("Returning arrow function from arrow function", () => { + util.testFunction` + const add = (x: number) => (y: number) => x + y; + return add(1)(2); + `.expectToMatchJsResult(); +}); + +test.each(["i++", "i--", "++i", "--i"])("Arrow function unary expression (%p)", lambda => { + util.testFunction` + let i = 10; + [1,2,3,4,5].forEach(() => ${lambda}); + return i; + `.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` + 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` + const add = (a = 3, b = 4) => a + b; + return add(${util.formatCode(...args)}); + `.expectToMatchJsResult(); +}); + +test("Function Expression", () => { + util.testFunction` + let add = function(a, b) {return a+b}; + return add(1,2); + `.expectToMatchJsResult(); +}); + +test("Function definition scope", () => { + util.testFunction` + function abc() { function xyz() { return 5; } } + function def() { function xyz() { return 3; } abc(); return xyz(); } + return def(); + `.expectToMatchJsResult(); +}); + +test("Function default parameter", () => { + util.testFunction` + function abc(defaultParam: string = "abc") { return defaultParam; } + return abc() + abc("def"); + `.expectToMatchJsResult(); +}); + +test.each([{ inp: [] }, { inp: [5] }, { inp: [1, 2] }])("Function Default Values (%p)", ({ inp }) => { + const callArgs = inp.join(","); + + util.testFunction( + `let add = function(a: number = 3, b: number = 4) { return a+b; }; + return add(${callArgs});` + ).expectToMatchJsResult(); +}); + +test("Function default array binding parameter", () => { + util.testFunction` + function foo([bar]: [string] = ["foobar"]) { + return bar; + } + return foo(); + `.expectToMatchJsResult(); +}); + +test("Function default object binding parameter", () => { + util.testFunction` + function foo({ bar }: { bar: string } = { bar: "foobar" }) { + return bar; + } + return foo(); + `.expectToMatchJsResult(); +}); + +test("Function default binding parameter maintains order", () => { + util.testFunction` + const resultsA = [{x: "foo"}, {x: "baz"}]; + const resultsB = ["blah", "bar"]; + let i = 0; + function a() { return resultsA[i++]; } + function b() { return resultsB[i++]; } + function foo({ x }: { x: string } = a(), y = b()) { + return x + y; + } + return foo(); + `.expectToMatchJsResult(); +}); + +test.each(["undefined", "null"])("Function default parameter with value %p", defaultValue => { + util.testFunction` + function foo(x = ${defaultValue}) { + return x; + } + return foo(); + ` + .expectToMatchJsResult() + .tap(builder => { + const lua = builder.getMainLuaCodeChunk(); + expect(lua).not.toMatch("if x == nil then"); + }) + .expectLuaToMatchSnapshot(); +}); + +test("Function default parameter with preceding statements", () => { + util.testFunction` + let i = 1 + function foo(x = i++) { + return x; + } + return [i, foo(), i]; + `.expectToMatchJsResult(); +}); + +test("Function default parameter with nil value and preceding statements", () => { + util.testFunction` + const a = new LuaTable() + a.set("foo", "bar") + function foo(x: any = a.set("foo", "baz")) { + return x ?? "nil"; + } + return [a.get("foo"), foo(), a.get("foo")]; + ` + .withLanguageExtensions() + .tap(builder => { + const lua = builder.getMainLuaCodeChunk(); + expect(lua).not.toMatch(" x = nil"); + }) + .expectToEqual(["bar", "nil", "baz"]); +}); + +test("Class method call", () => { + util.testFunction` + class TestClass { + public classMethod(): number { return 4; } + } + + const classInstance = new TestClass(); + return classInstance.classMethod(); + `.expectToMatchJsResult(); +}); + +test("Class dot method call void", () => { + util.testFunction` + class TestClass { + public dotMethod: () => number = () => 4; + } + + const classInstance = new TestClass(); + return classInstance.dotMethod(); + `.expectToMatchJsResult(); +}); + +test("Class dot method call with parameter", () => { + util.testFunction` + class TestClass { + public dotMethod: (x: number) => number = x => 3 * x; + } + + const classInstance = new TestClass(); + return classInstance.dotMethod(4); + `.expectToMatchJsResult(); +}); + +test("Class static dot method", () => { + util.testFunction` + class TestClass { + public static dotMethod: () => number = () => 4; + } + + return TestClass.dotMethod(); + `.expectToMatchJsResult(); +}); + +test("Class static dot method with parameter", () => { + util.testFunction` + class TestClass { + public static dotMethod: (x: number) => number = x => 3 * x; + } + + return TestClass.dotMethod(4); + `.expectToMatchJsResult(); +}); + +const functionTypeDeclarations = [ + ["arrow", ": (...args: any) => any"], + ["call signature", ": { (...args: any): any; }"], + ["generic", ": Function"], + ["inferred", ""], +]; + +test.each(functionTypeDeclarations)("Function bind (%s)", (_, type) => { + util.testFunction` + const abc${type} = function (this: { a: number }, a: string, b: string) { return this.a + a + b; } + return abc.bind({ a: 4 }, "b")("c"); + `.expectToMatchJsResult(); +}); + +test.each(functionTypeDeclarations)("Function apply (%s)", (_, type) => { + util.testFunction` + const abc${type} = function (this: { a: number }, a: string) { return this.a + a; } + return abc.apply({ a: 4 }, ["b"]); + `.expectToMatchJsResult(); +}); + +// Fix #1226: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1226 +test.each(functionTypeDeclarations)("function apply without arguments should not lead to exception (%s)", (_, type) => { + util.testFunction` + const f${type} = function (this: number) { return this + 3; } + return f.apply(4); + `.expectToMatchJsResult(); +}); + +// Issue #1218: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1218 +test.each(["() => 4", "undefined"])("prototype call on nullable function (%p)", value => { + util.testFunction` + function call(f?: () => number) { + return f?.apply(3); + } + return call(${value}); + ` + .setOptions({ strictNullChecks: true }) + .expectToMatchJsResult(); +}); + +test.each(functionTypeDeclarations)("Function call (%s)", (_, type) => { + util.testFunction` + const abc${type} = function (this: { a: number }, a: string) { return this.a + a; } + return abc.call({ a: 4 }, "b"); + `.expectToMatchJsResult(); +}); + +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;", + "const wrap = (fn: (...args: any[]) => any) => (...args: any[]) => fn(...args); const fn = wrap((x, y, z) => {});", +])("function.length (%p)", declaration => { + util.testFunction` + ${declaration} + return fn.length; + `.expectToMatchJsResult(); +}); + +test.each([tstl.LuaTarget.Lua50, tstl.LuaTarget.Lua51, tstl.LuaTarget.Universal])( + "function.length unsupported (%p)", + luaTarget => { + util.testFunction` + function fn() {} + return fn.length; + ` + .setOptions({ luaTarget }) + .expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]); + } +); + +test("Recursive function definition", () => { + util.testFunction` + function f() { return typeof f; }; + return f(); + `.expectToMatchJsResult(); +}); + +test("Recursive function expression", () => { + util.testFunction` + let f = function() { return typeof f; }; + return f(); + `.expectToMatchJsResult(); +}); + +test("Wrapped recursive function expression", () => { + util.testFunction` + function wrap(fn: T) { return fn; } + let f = wrap(function() { return typeof f; }); return f(); + `.expectToMatchJsResult(); +}); + +test("Recursive arrow function", () => { + util.testFunction` + let f = () => typeof f; + return f(); + `.expectToMatchJsResult(); +}); + +test("Wrapped recursive arrow function", () => { + util.testFunction` + function wrap(fn: T) { return fn; } + let f = wrap(() => typeof f); + return f(); + `.expectToMatchJsResult(); +}); + +test("Object method declaration", () => { + util.testFunction` + let o = { v: 4, m(i: number): number { return this.v * i; } }; + return o.m(3); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1266 +test("Object method declaration used with non-null operator (#1266)", () => { + util.testFunction` + let o = { v: 4, m(i: number): number { return this.v * i; } }; + return o.m!(3); + `.expectToMatchJsResult(); +}); + +test.each([ + { args: ["bar"], expected: "foobar" }, + { args: ["baz", "bar"], expected: "bazbar" }, +])("Function overload (%p)", ({ args, expected }) => { + util.testFunction` + class O { + prop = "foo"; + method(s: string): string; + method(this: void, s1: string, s2: string): string; + method(s1: string) { + if (typeof this === "string") { + return this + s1; + } + return this.prop + s1; + } + }; + const o = new O(); + return o.method(${util.formatCode(...args)}); + `.expectToEqual(expected); +}); + +test("Nested Function", () => { + util.testFunction` + class C { + private prop = "bar"; + public outer() { + const o = { + prop: "foo", + innerFunc: function() { return this.prop; }, + innerArrow: () => this.prop + }; + return o.innerFunc() + o.innerArrow(); + } + } + let c = new C(); + return c.outer(); + `.expectToMatchJsResult(); +}); + +test.each([ + { s1: "abc", s2: "abc" }, + { s1: "abc", s2: "def" }, +])("Dot vs Colon method call (%p)", ({ s1, s2 }) => { + util.testFunction` + class MyClass { + dotMethod(this: void, s: string) { + return s; + } + colonMethod(s: string) { + return s; + } + } + const inst = new MyClass(); + return inst.dotMethod("${s1}") == inst.colonMethod("${s2}"); + `.expectToMatchJsResult(); +}); + +test("Element access call", () => { + util.testFunction` + class C { + prop = "bar"; + method(s: string) { return s + this.prop; } + } + const c = new C(); + return c['method']("foo"); + `.expectToMatchJsResult(); +}); + +test("Element access call no args", () => { + util.testFunction` + class C { + prop = "bar"; + method() { return this.prop; } + } + const c = new C(); + return c['method'](); + `.expectToMatchJsResult(); +}); + +test("Complex element access call", () => { + util.testFunction` + class C { + prop = "bar"; + method(s: string) { return s + this.prop; } + } + function getC() { return new C(); } + return getC()['method']("foo"); + `.expectToMatchJsResult(); +}); + +test("Complex element access call no args", () => { + util.testFunction` + class C { + prop = "bar"; + method() { return this.prop; } + } + function getC() { return new C(); } + return getC()['method'](); + `.expectToMatchJsResult(); +}); + +test("Complex element access call statement", () => { + util.testFunction` + let foo: string; + class C { + prop = "bar"; + method(s: string) { foo = s + this.prop; } + } + function getC() { return new C(); } + getC()['method']("foo"); + return foo; + `.expectToMatchJsResult(); +}); + +test("Function local overriding export", () => { + util.testModule` + export const foo = 5; + function bar(foo: number) { + return foo; + } + export const result = bar(7); + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("Function using global as this", () => { + // Value is provided with top-level return with ts-ignore, because modules are always strict. + // TODO: Provide a different builder kind for such tests? + util.testModule` + (globalThis as any).foo = "foo"; + function bar(this: any) { + return this.foo; + } + + // @ts-ignore + return bar(); + `.expectToEqual("foo"); +}); + +test("Function rest binding pattern", () => { + util.testFunction` + function bar(foo: string, ...[bar, baz]: [string, string]) { + return bar + baz + foo; + } + return bar("abc", "def", "xyz"); + `.expectToMatchJsResult(); +}); + +test("Function rest parameter", () => { + util.testFunction` + function foo(a: unknown, ...b: string[]) { + return b.join(""); + } + return foo("A", "B", "C", "D"); + `.expectToMatchJsResult(); +}); + +test("Function nested rest parameter", () => { + util.testFunction` + function foo(a: unknown, ...b: string[]) { + function bar() { + return b.join(""); + } + return bar(); + } + return foo("A", "B", "C", "D"); + `.expectToMatchJsResult(); +}); + +test("Function nested rest spread", () => { + util.testFunction` + function foo(a: unknown, ...b: string[]) { + function bar() { + const c = [...b]; + return c.join(""); + } + return bar(); + } + return foo("A", "B", "C", "D"); + `.expectToMatchJsResult(); +}); + +test("Function rest parameter (unreferenced)", () => { + util.testFunction` + function foo(a: unknown, ...b: string[]) { + return "foobar"; + } + return foo("A", "B", "C", "D"); + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toMatch("{...}")) + .expectToMatchJsResult(); +}); + +test("Function rest parameter (referenced in property shorthand)", () => { + util.testFunction` + function foo(a: unknown, ...b: string[]) { + const c = { b }; + return c.b.join(""); + } + return foo("A", "B", "C", "D"); + `.expectToMatchJsResult(); +}); + +test("named function expression reference", () => { + util.testFunction` + const y = function x() { + return { x: typeof x, y: typeof y }; + }; + + return y(); + `.expectToMatchJsResult(); +}); + +test("missing declaration name", () => { + util.testModule` + function () {} + `.expectDiagnosticsToMatchSnapshot([1003]); +}); + +test("top-level function declaration is global", () => { + // Can't be tested with expectToMatchJsResult because in JS that would not be global + util.testModule` + import './a'; + export const result = foo(); + ` + .addExtraFile("a.ts", 'function foo() { return "foo" }') + .expectToEqual({ result: "foo" }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1325 +test("call expression should not throw (#1325)", () => { + util.testModule` + function test(iterator:Iterator) { + iterator.return?.(); + } + ` + // Note: does not reproduce without strict=true + .setOptions({ target: ts.ScriptTarget.ESNext, strict: true }) + .expectToHaveNoDiagnostics(); +}); diff --git a/test/unit/functions/generators.spec.ts b/test/unit/functions/generators.spec.ts new file mode 100644 index 000000000..aa62d719d --- /dev/null +++ b/test/unit/functions/generators.spec.ts @@ -0,0 +1,171 @@ +import { LuaTarget } from "../../../src/CompilerOptions"; +import { unsupportedForTarget } from "../../../src/transformation/utils/diagnostics"; +import * as util from "../../util"; + +test("generator parameters", () => { + util.testFunction` + function* generator(value: number) { + yield value; + } + + return generator(5).next(); + `.expectToMatchJsResult(); +}); + +test(".next()", () => { + util.testFunction` + function* generator() { + yield 1; + yield 2; + return 3; + } + + const it = generator(); + return [it.next(), it.next(), it.next(), it.next()]; + `.expectToMatchJsResult(); +}); + +test(".next() with parameters", () => { + util.testFunction` + function* generator() { + return yield 0; + } + + const it = generator(); + return [it.next(1), it.next(2), it.next(3)]; + `.expectToMatchJsResult(); +}); + +test("for..of", () => { + util.testFunction` + function* generator() { + yield 1; + yield 2; + yield undefined; + return 3; + } + + const results = []; + for (const value of generator()) { + results.push({ value }); + } + return results; + `.expectToMatchJsResult(); +}); + +describe("yield*", () => { + test("generator", () => { + util.testFunction` + function* subGenerator() { + yield 1; + yield 2; + yield 3; + } + + function* generator() { + yield 0; + return yield* subGenerator(); + } + + const it = generator(); + return [it.next(), it.next(), it.next(), it.next(), it.next()]; + `.expectToMatchJsResult(); + }); + + test("array", () => { + util.testFunction` + function* generator() { + return yield* [1, 2, 3]; + } + + const it = generator(); + return [it.next(), it.next(), it.next(), it.next()]; + `.expectToMatchJsResult(); + }); + + test("string", () => { + util.testFunction` + function* generator() { + return yield* "abc"; + } + + const it = generator(); + return [it.next(), it.next(), it.next(), it.next()]; + `.expectToMatchJsResult(); + }); + + test("iterable", () => { + util.testFunction` + function* generator() { + return yield* new Set([1, 2, 3]); + } + + const it = generator(); + return [it.next(), it.next(), it.next(), it.next()]; + `.expectToMatchJsResult(); + }); +}); + +test("function expression", () => { + util.testFunction` + const generator = function*() { + return true; + } + + return generator().next(); + `.expectToMatchJsResult(); +}); + +test("class method", () => { + util.testFunction` + class A { + *generator() { + return true; + } + } + + return new A().generator().next(); + `.expectToMatchJsResult(); +}); + +test("object member", () => { + util.testFunction` + const a = { + *generator() { + return true; + } + } + + return a.generator().next(); + `.expectToMatchJsResult(); +}); + +test("hoisting", () => { + util.testFunction` + return generator().next(); + + function* generator() { + return true; + } + `.expectToMatchJsResult(); +}); + +util.testEachVersion( + "generator yield inside try/catch", + () => util.testFunction` + function* generator() { + try { + yield 4; + } catch { + throw "something went wrong"; + } + } + return generator().next(); + `, + // Cannot execute LuaJIT with test runner + { + ...util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()), + [LuaTarget.Lua50]: builder => builder.expectToHaveDiagnostics([unsupportedForTarget.code]), + [LuaTarget.Lua51]: builder => builder.expectToHaveDiagnostics([unsupportedForTarget.code]), + } +); diff --git a/test/unit/functions/noImplicitGlobalVariables.spec.ts b/test/unit/functions/noImplicitGlobalVariables.spec.ts new file mode 100644 index 000000000..00bd6195a --- /dev/null +++ b/test/unit/functions/noImplicitGlobalVariables.spec.ts @@ -0,0 +1,30 @@ +import * as util from "../../util"; + +test("normal TSTL creates global variables", () => { + const builder = util.testModule` + function foo() {} + const bar = 123; + `.expectToHaveNoDiagnostics(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).not.toContain("local"); +}); + +test("noImplicitGlobalVariables does not create any global variables", () => { + const builder = util.testModule` + function foo() {} + const bar = 123; + ` + .setOptions({ noImplicitGlobalVariables: true }) + .expectToHaveNoDiagnostics(); + + const transpiledFile = builder.getLuaResult().transpiledFiles[0]; + expect(transpiledFile).toBeDefined(); + const { lua } = transpiledFile; + expect(lua).toBeDefined(); + expect(lua).toContain("local function foo("); + expect(lua).toContain("local bar ="); +}); diff --git a/test/unit/functions/noImplicitSelfOption.spec.ts b/test/unit/functions/noImplicitSelfOption.spec.ts new file mode 100644 index 000000000..ef788c922 --- /dev/null +++ b/test/unit/functions/noImplicitSelfOption.spec.ts @@ -0,0 +1,95 @@ +import * as path from "path"; +import { transpileFiles } from "../../../src"; +import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; +import * as util from "../../util"; + +test("enables noSelfInFile behavior for functions", () => { + util.testFunction` + function fooBar() {} + const test: (this: void) => void = fooBar; + fooBar(); + ` + .setOptions({ noImplicitSelf: true }) + .expectToHaveNoDiagnostics(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1084 +test.each(["\\", "/"])("transpileFiles handles paths with noImplicitSelf and %s separator (#1084)", separator => { + const projectDir = `${path.dirname(path.dirname(__dirname))}${separator}transpile${separator}project`; + const emittedFiles: Record = {}; + const { diagnostics } = transpileFiles( + [ + `${projectDir}${separator}index.ts`, + `${projectDir}${separator}api.d.ts`, + `${projectDir}${separator}otherFile.ts`, + ], + { noImplicitSelf: true }, + (fileName, text) => (emittedFiles[fileName] = text) + ); + expect(diagnostics).toHaveLength(0); + expect(Object.keys(emittedFiles)).not.toHaveLength(0); + for (const fileContent of Object.values(emittedFiles)) { + expect(fileContent).toContain("getNumber()"); + expect(fileContent).not.toContain("getNumber(self)"); + expect(fileContent).not.toContain("getNumber(_G)"); + } +}); + +test("noImplicitSelf does not affect functions in default libraries", () => { + util.testFunction` + const array = [1, 2, 3]; + const items = array.filter(x => x > 1); // array.filter is in external library + return items; + ` + .setOptions({ + noImplicitSelf: true, + }) + .expectToMatchJsResult(); +}); + +test("enables noSelfInFile behavior for methods", () => { + util.testFunction` + class FooBar { + fooBar() {} + } + const fooBar = new FooBar(); + const test: (this: any) => void = fooBar.fooBar; + ` + .setOptions({ noImplicitSelf: true }) + .expectToHaveNoDiagnostics(); +}); + +test("generates declaration files with @noSelfInFile", () => { + const fooBuilder = util.testModule` + export function bar() {} + ` + .setOptions({ declaration: true, noImplicitSelf: true }) + .expectToHaveNoDiagnostics(); + + const fooDeclaration = fooBuilder.getLuaResult().transpiledFiles.find(f => f.declaration)?.declaration; + util.assert(fooDeclaration !== undefined); + + expect(fooDeclaration).toContain("@noSelfInFile"); + + util.testModule` + import { bar } from "./foo"; + const test: (this: void) => void = bar; + ` + .addExtraFile("foo.d.ts", fooDeclaration) + .ignoreDiagnostics([couldNotResolveRequire.code]) // no foo implementation in the project to create foo.lua + .expectToHaveNoDiagnostics(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1292 +test("explicit this parameter respected over noImplicitSelf", () => { + util.testModule` + function foo(this: unknown, arg: any) { + return {self: this, arg}; + } + export const result = foo(1); + ` + .setOptions({ + noImplicitSelf: true, + }) + .expectToMatchJsResult(); +}); diff --git a/test/unit/functions/noSelfAnnotation.spec.ts b/test/unit/functions/noSelfAnnotation.spec.ts new file mode 100644 index 000000000..f8418fabd --- /dev/null +++ b/test/unit/functions/noSelfAnnotation.spec.ts @@ -0,0 +1,132 @@ +import * as util from "../../util"; + +const methodHolders = ["class", "interface"]; + +test("@noSelf on declared function removes context argument", () => { + util.testModule` + /** @noSelf */ + declare function myFunction(): void; + myFunction(); + `.expectLuaToMatchSnapshot(); +}); + +test.each(methodHolders)("@noSelf on method inside %s declaration removes context argument", holderType => { + util.testModule` + declare ${holderType} MethodHolder { + /** @noSelf */ + myMethod(): void; + } + declare const holder: MethodHolder; + holder.myMethod(); + `.expectLuaToMatchSnapshot(); +}); + +test.each(methodHolders)("@noSelf on parent %s declaration removes context argument", holderType => { + util.testModule` + /** @noSelf */ + declare ${holderType} MethodHolder { + myMethod(): void; + } + declare const holder: MethodHolder; + holder.myMethod(); + `.expectLuaToMatchSnapshot(); +}); + +test("@noSelf on method inside namespace declaration removes context argument", () => { + util.testModule` + declare namespace MyNamespace { + /** @noSelf */ + export function myMethod(): void; + } + MyNamespace.myMethod(); + `.expectLuaToMatchSnapshot(); +}); + +test("@noSelf on parent namespace declaration removes context argument", () => { + util.testModule` + /** @noSelf */ + declare namespace MyNamespace { + export function myMethod(): void; + } + MyNamespace.myMethod(); + `.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(); +}); + +// additional coverage for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1292 +test("explicit this parameter respected over @noSelf", () => { + util.testModule` + /** @noSelfInFile */ + function foo(this: unknown, arg: any) { + return {self: this, arg}; + } + export const result = foo(1); + `.expectToMatchJsResult(); +}); + +test("respect noSelfInFile over noImplicitSelf", () => { + const result = util.testModule` + /** @noSelfInFile **/ + const func: Function = () => 1; + export const result = func(1); + ` + .expectToMatchJsResult() + .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("func(1)"); +}); + +test("respect noSelfInFile over noImplicitSelf (func declared in other file)", () => { + const result = util.testModule` + import { func, result } from "./functions"; + + export const result1 = result; + export const result2 = func(1); + ` + .addExtraFile( + "functions.ts", + ` + /** @noSelfInFile **/ + export const func: Function = () => 1; + export const result = func(2); + ` + ) + .expectToMatchJsResult() + .getLuaResult(); + + expect(result.transpiledFiles).not.toHaveLength(0); + + const mainFile = result.transpiledFiles.find(f => f.outPath.includes("main.lua")); + expect(mainFile).toBeDefined(); + const functionFile = result.transpiledFiles.find(f => f.outPath.includes("functions.lua")); + expect(functionFile).toBeDefined(); + + // avoid ts error "not defined", even though toBeDefined is being checked above + if (!mainFile || !functionFile) return; + + expect(mainFile.lua).toBeDefined(); + expect(mainFile.lua).toContain("func(1)"); + expect(mainFile.lua).toBeDefined(); + expect(functionFile.lua).toContain("func(2)"); +}); diff --git a/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap b/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap new file mode 100644 index 000000000..b7175df75 --- /dev/null +++ b/test/unit/functions/validation/__snapshots__/invalidFunctionAssignments.spec.ts.snap @@ -0,0 +1,3051 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Invalid function argument ({"definition": "/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } + namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }", "value": "AnonFuncNSMergedNoSelfClass.nsFunc"}): 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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(11,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): 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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}): 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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}): 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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}): 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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}): 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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}): 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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}): 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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(8,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}): 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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}): 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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}): 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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}): 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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}): 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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}): 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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"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: 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) { 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: 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) { 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'. +main.ts(4,23): 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 argument with cast ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): diagnostics 2`] = ` +"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'. +main.ts(4,23): 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 argument with cast ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): diagnostics 1`] = ` +"main.ts(4,23): 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'. +main.ts(4,23): 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": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): diagnostics 2`] = ` +"main.ts(4,23): 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'. +main.ts(4,23): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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'. +main.ts(4,23): 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 argument with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 2`] = ` +"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'. +main.ts(4,23): 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 argument with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 3`] = ` +"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'. +main.ts(4,23): 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 argument with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 4`] = ` +"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'. +main.ts(4,23): 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 ({"definition": "/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } + namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }", "value": "AnonFuncNSMergedNoSelfClass.nsFunc"}): 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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(11,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): 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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}): 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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}): 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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}): 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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}): 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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}): 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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}): 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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(8,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}): 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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}): 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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}): 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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}): 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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}): 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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}): 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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"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: 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) { 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: 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) { 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'. +main.ts(4,14): 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 2`] = ` +"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'. +main.ts(4,14): 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": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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'. +main.ts(4,14): 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": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): diagnostics 2`] = ` +"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'. +main.ts(4,14): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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'. +main.ts(4,14): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 2`] = ` +"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'. +main.ts(4,14): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 3`] = ` +"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'. +main.ts(4,14): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 4`] = ` +"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'. +main.ts(4,14): 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 generic argument ({"definition": "/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } + namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }", "value": "AnonFuncNSMergedNoSelfClass.nsFunc"}): 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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(11,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): 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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}): 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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}): 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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}): 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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}): 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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}): 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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}): 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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(8,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}): 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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}): 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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}): 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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}): 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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}): 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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}): 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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"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: 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) { 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: 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) { 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'."`; + +exports[`Invalid function overload assignment ("(this: void, s1: string, s2: string) => string"): diagnostics 1`] = `"main.ts(7,65): error TSTL: Unsupported assignment of function with different overloaded types for 'this'. Overloads should all have the same type for 'this'."`; + +exports[`Invalid function overload assignment ("{(this: any, s1: string, s2: string): string}"): diagnostics 1`] = `"main.ts(7,64): error TSTL: Unsupported assignment of function with different overloaded types for 'this'. Overloads should all have the same type for 'this'."`; + +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'."`; + +exports[`Invalid function return ({"definition": "/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } + namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }", "value": "AnonFuncNSMergedNoSelfClass.nsFunc"}): 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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(11,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}): diagnostics 1`] = `"main.ts(7,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 ({"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 no 'this' parameter to function with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; + +exports[`Invalid function return ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}): 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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}): 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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}): 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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}): 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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}): 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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}): 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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(8,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}): 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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}): 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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}): 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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(10,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}): 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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}): 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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}): 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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 2`] = `"main.ts(6,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 ({"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: 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) { 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: 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) { 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'. +main.ts(4,24): 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 2`] = ` +"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'. +main.ts(4,24): 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": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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'. +main.ts(4,24): 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 with cast ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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'. +main.ts(4,24): 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 with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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'. +main.ts(4,24): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 2`] = ` +"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'. +main.ts(4,24): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 3`] = ` +"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'. +main.ts(4,24): 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": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): diagnostics 4`] = ` +"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'. +main.ts(4,24): 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 tuple assignment: diagnostics 1`] = ` +"main.ts(5,13): error TS2322: Type '[number, Meth]' is not assignable to type '[number, Func]'. + Type at position 1 in source is not compatible with type at position 1 in target. + Type 'Meth' is not assignable to type 'Func'. + The 'this' types of each signature are incompatible. + Type 'void' is not assignable to type '{}'. +main.ts(5,38): 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 ({"definition": "/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } + namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }", "value": "AnonFuncNSMergedNoSelfClass.nsFunc"}): 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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): 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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}): diagnostics 1`] = `"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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}): diagnostics 1`] = `"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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}): 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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}): 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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}): 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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}): 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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}): 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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 1`] = `"main.ts(4,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}): diagnostics 2`] = `"main.ts(4,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}): diagnostics 2`] = `"main.ts(7,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}): diagnostics 2`] = `"main.ts(5,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}): diagnostics 2`] = `"main.ts(6,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}): 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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}): diagnostics 1`] = `"main.ts(6,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}): 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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}): 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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}): diagnostics 2`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}): diagnostics 2`] = `"main.ts(8,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 1`] = `"main.ts(8,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}): diagnostics 2`] = `"main.ts(8,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}): 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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}): 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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}): 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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}): 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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}): 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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 1`] = `"main.ts(4,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}): diagnostics 2`] = `"main.ts(4,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}): diagnostics 2`] = `"main.ts(5,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 ({"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: 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) { 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: 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) { 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'."`; + +exports[`Invalid lua lib function argument: diagnostics 1`] = `"main.ts(4,19): error TSTL: Unable to convert function with no 'this' parameter to function 'callbackfn' with 'this'. To fix, wrap in an arrow function, or declare with 'this: any'."`; + +exports[`Invalid method tuple assignment: diagnostics 1`] = `"main.ts(5,38): 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 object with method argument ({"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,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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(11,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}, "(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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}, "(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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}, "(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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}, "(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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}, "(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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}, "(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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}, "(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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}, "(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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}, "(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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}, "(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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}, "(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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}, "(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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}, "(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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}, "(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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}, "(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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}, "(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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"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: 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) { 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: 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) { 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'."`; + +exports[`Invalid object with method assignment ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(11,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}, "(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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}, "(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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}, "(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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}, "(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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}, "(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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}, "(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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(8,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}, "(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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(7,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}, "(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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}, "(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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(10,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(9,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}, "(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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}, "(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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}, "(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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}, "(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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}, "(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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}, "(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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}, "(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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}, "(this: void, s: string) => string", false): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(this: any, s: string) => string", true): diagnostics 1`] = `"main.ts(6,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 ({"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: 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) { 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: 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) { 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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "<(this: any, s: string) => string>(noSelfInFileFunc)", 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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}, "(selfFunc) as ((this: void, 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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}, "<(this: void, s: string) => string>(selfFunc)", 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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(voidFunc) as ((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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(voidFunc) 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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "<(s: string) => string>(voidFunc)", 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'. +main.ts(4,24): 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 object with method assignment with cast ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "<(this: any, s: string) => string>(voidFunc)", 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'. +main.ts(4,24): 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 object with method variable declaration ({"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"): 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 ({"definition": "/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();", "value": "anonFunctionNestedInNoSelfClass"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();", "value": "noSelfAnonMethodClassMergedNS.method"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();", "value": "noSelfFuncPropClass.noSelfFuncProp"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }", "value": "NoSelfStaticFuncPropClass.noSelfStaticFuncProp"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }", "value": "NoSelfStaticMethodClass.noSelfStaticMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();", "value": "noSelfMethodClassExpression.noSelfMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };", "value": "noSelfFuncPropInterface.noSelfFuncProp"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();", "value": "anonFunctionNestedInClassInNoSelfNs"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(9,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 ({"definition": "/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();", "value": "anonMethodClassInNoSelfNs.method"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(8,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 ({"definition": "/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };", "value": "anonMethodInterfaceInNoSelfNs.method"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(10,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }", "value": "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(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 ({"definition": "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }", "value": "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfLambdaNs.noSelfNsLambda"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();", "value": "noSelfInFileFuncNestedInClass"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", "value": "noSelfInFileFunc"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}, "(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 ({"definition": "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", "value": "noSelfInFileLambda"}, "(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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }", "value": "NoSelfInFileFuncNs.noSelfInFileNsFunc"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }", "value": "NoSelfInFileLambdaNs.noSelfInFileNsLambda"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();", "value": "anonFuncPropClass.anonFuncProp"}, "(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 ({"definition": "class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();", "value": "anonMethodClass.anonMethod"}, "(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 ({"definition": "class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();", "value": "anonMethodClassMergedNoSelfNS.method"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }", "value": "AnonStaticFuncPropClass.anonStaticFuncProp"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", "value": "AnonStaticMethodClass.anonStaticMethod"}, "(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 ({"definition": "class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();", "value": "funcPropClass.funcProp"}, "(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 ({"definition": "class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();", "value": "methodClass.method"}, "(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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedClass.nsFunc"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();", "value": "noSelfMethodClass.noSelfMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(7,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 ({"definition": "class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }", "value": "StaticFuncPropClass.staticFuncProp"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }", "value": "StaticMethodClass.staticMethod"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }", "value": "StaticVoidFuncPropClass.staticVoidFuncProp"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }", "value": "StaticVoidMethodClass.staticVoidMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();", "value": "voidFuncPropClass.voidFuncProp"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();", "value": "voidMethodClass.voidMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };", "value": "anonFuncPropInterface.anonFuncProp"}, "(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 ({"definition": "interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };", "value": "anonMethodInterface.anonMethod"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(6,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 ({"definition": "interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };", "value": "funcPropInterface.funcProp"}, "(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 ({"definition": "interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };", "value": "methodInterface.method"}, "(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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };", "value": "noSelfMethodInterface.noSelfMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(9,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(8,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 ({"definition": "interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };", "value": "voidFuncPropInterface.voidFuncProp"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(8,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(8,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 ({"definition": "interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };", "value": "voidMethodInterface.voidMethod"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(8,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 ({"definition": "let anonFunc: {(s: string): string} = function(s) { return s; };", "value": "anonFunc"}, "(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 ({"definition": "let anonLambda: (s: string) => string = s => s;", "value": "anonLambda"}, "(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 ({"definition": "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", "value": "selfFunc"}, "(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 ({"definition": "let selfLambda: (this: any, s: string) => string = s => s;", "value": "selfLambda"}, "(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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(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 ({"definition": "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", "value": "voidFunc"}, "(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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}, "(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 ({"definition": "let voidLambda: (this: void, s: string) => string = s => s;", "value": "voidLambda"}, "(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 ({"definition": "namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }", "value": "FuncNestedNs.NestedNs.nestedNsFunc"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace FuncNs { export function nsFunc(s: string) { return s; } }", "value": "FuncNs.nsFunc"}, "(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 ({"definition": "namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }", "value": "LambdaNestedNs.NestedNs.nestedNsLambda"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }", "value": "LambdaNs.nsLambda"}, "(this: void, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }", "value": "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(4,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"definition": "namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }", "value": "NoSelfFuncNs.noSelfNsFunc"}, "(this: any, s: string) => string"): diagnostics 1`] = `"main.ts(5,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 ({"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: 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) { 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: 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) { 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 new file mode 100644 index 000000000..abac4e483 --- /dev/null +++ b/test/unit/functions/validation/functionExpressionTypeInference.spec.ts @@ -0,0 +1,325 @@ +import * as util from "../../../util"; + +test.each(["noSelf", "noSelfInFile"])("noSelf function method argument (%p)", noSelfTag => { + const header = ` + /** @${noSelfTag} */ namespace NS { + export class C { + method(fn: (s: string) => string) { return fn("foo"); } + } + } + function foo(this: void, s: string) { return s; } + `; + const code = ` + const c = new NS.C(); + return c.method(foo); + `; + util.testFunction(code).setTsHeader(header).expectToMatchJsResult(); +}); + +test("noSelfInFile works when first statement has other annotations", () => { + util.testModule` + /** @noSelfInFile */ + + /** @internal */ + function foo() {} + + const test: (this: void) => void = foo; + `.expectToHaveNoDiagnostics(); +}); + +test.each(["(this: void, s: string) => string", "(this: any, s: string) => string", "(s: string) => string"])( + "Function expression type inference in binary operator (%p)", + funcType => { + const header = `let undefinedFunc: (${funcType}) | undefined;`; + util.testFunction` + let func: ${funcType} = s => s; + func = undefinedFunc || (s => s); + return func("foo"); + ` + .setTsHeader(header) + .expectToMatchJsResult(); + } +); + +test.each(["s => s", "(s => s)", "function(s) { return s; }", "(function(s) { return s; })"])( + "Function expression type inference in class (%p)", + funcExp => { + util.testFunction` + class Foo { + func: (this: void, s: string) => string = ${funcExp}; + method: (s: string) => string = ${funcExp}; + static staticFunc: (this: void, s: string) => string = ${funcExp}; + static staticMethod: (s: string) => string = ${funcExp}; + } + const foo = new Foo(); + return foo.func("a") + foo.method("b") + Foo.staticFunc("c") + Foo.staticMethod("d"); + `.expectToMatchJsResult(); + } +); + +test.each([ + { assignTo: "const foo: Foo", funcExp: "s => s" }, + { assignTo: "const foo: Foo", funcExp: "(s => s)" }, + { assignTo: "const foo: Foo", funcExp: "function(s) { return s; }" }, + { assignTo: "const foo: Foo", funcExp: "(function(s) { return s; })" }, + { assignTo: "let foo: Foo; foo", funcExp: "s => s" }, + { assignTo: "let foo: Foo; foo", funcExp: "(s => s)" }, + { assignTo: "let foo: Foo; foo", funcExp: "function(s) { return s; }" }, + { assignTo: "let foo: Foo; foo", funcExp: "(function(s) { return s; })" }, +])("Function expression type inference in object literal (%p)", ({ assignTo, funcExp }) => { + util.testFunction` + interface Foo { + func(this: void, s: string): string; + method(this: this, s: string): string; + } + ${assignTo} = {func: ${funcExp}, method: ${funcExp}}; + return foo.method("foo") + foo.func("bar"); + `.expectToMatchJsResult(); +}); + +test("Function expression type inference in object literal assigned to narrower type", () => { + util.testFunction` + let foo: {} = {bar: s => s}; + return (foo as {bar: (a: any) => any}).bar("foobar"); + `.expectToMatchJsResult(); +}); + +test.each([ + { assignTo: "const foo: Foo", funcExp: "s => s" }, + { assignTo: "const foo: Foo", funcExp: "(s => s)" }, + { assignTo: "const foo: Foo", funcExp: "function(s) { return s; }" }, + { assignTo: "const foo: Foo", funcExp: "(function(s) { return s; })" }, + { assignTo: "let foo: Foo; foo", funcExp: "s => s" }, + { assignTo: "let foo: Foo; foo", funcExp: "(s => s)" }, + { assignTo: "let foo: Foo; foo", funcExp: "function(s) { return s; }" }, + { assignTo: "let foo: Foo; foo", funcExp: "(function(s) { return s; })" }, +])("Function expression type inference in object literal (generic key) (%p)", ({ assignTo, funcExp }) => { + util.testFunction` + interface Foo { + [f: string]: (this: void, s: string) => string; + } + ${assignTo} = {func: ${funcExp}}; + return foo.func("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { + assignTo: "const funcs: [Func, Method]", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "s => s", + }, + { + assignTo: "const funcs: [Func, Method]", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "(s => s)", + }, + { + assignTo: "const funcs: [Func, Method]", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "function(s) { return s; }", + }, + { + assignTo: "const funcs: [Func, Method]", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "(function(s) { return s; })", + }, + { + assignTo: "let funcs: [Func, Method]; funcs", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "s => s", + }, + { + assignTo: "let funcs: [Func, Method]; funcs", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "(s => s)", + }, + { + assignTo: "let funcs: [Func, Method]; funcs", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "function(s) { return s; }", + }, + { + assignTo: "let funcs: [Func, Method]; funcs", + func: "funcs[0]", + method: "funcs[1]", + funcExp: "(function(s) { return s; })", + }, + { + assignTo: "const [func, meth]: [Func, Method]", + func: "func", + method: "meth", + funcExp: "s => s", + }, + { + assignTo: "const [func, meth]: [Func, Method]", + func: "func", + method: "meth", + funcExp: "(s => s)", + }, + { + assignTo: "const [func, meth]: [Func, Method]", + func: "func", + method: "meth", + funcExp: "function(s) { return s; }", + }, + { + assignTo: "const [func, meth]: [Func, Method]", + func: "func", + method: "meth", + funcExp: "(function(s) { return s; })", + }, + { + assignTo: "let func: Func; let meth: Method; [func, meth]", + func: "func", + method: "meth", + funcExp: "s => s", + }, + { + assignTo: "let func: Func; let meth: Method; [func, meth]", + func: "func", + method: "meth", + funcExp: "(s => s)", + }, + { + assignTo: "let func: Func; let meth: Method; [func, meth]", + func: "func", + method: "meth", + funcExp: "function(s) { return s; }", + }, + { + assignTo: "let func: Func; let meth: Method; [func, meth]", + func: "func", + method: "meth", + funcExp: "(function(s) { return s; })", + }, +])("Function expression type inference in tuple (%p)", ({ assignTo, func, method, funcExp }) => { + util.testFunction` + interface Foo { + method(s: string): string; + } + interface Func { + (this: void, s: string): string; + } + interface Method { + (this: Foo, s: string): string; + } + ${assignTo} = [${funcExp}, ${funcExp}]; + const foo: Foo = {method: ${method}}; + return foo.method("foo") + ${func}("bar"); + `.expectToMatchJsResult(); +}); + +test.each([ + { assignTo: "const meths: Method[]", method: "meths[0]", funcExp: "s => s" }, + { assignTo: "const meths: Method[]", method: "meths[0]", funcExp: "(s => s)" }, + { assignTo: "const meths: Method[]", method: "meths[0]", funcExp: "function(s) { return s; }" }, + { assignTo: "const meths: Method[]", method: "meths[0]", funcExp: "(function(s) { return s; })" }, + { assignTo: "let meths: Method[]; meths", method: "meths[0]", funcExp: "s => s" }, + { assignTo: "let meths: Method[]; meths", method: "meths[0]", funcExp: "(s => s)" }, + { assignTo: "let meths: Method[]; meths", method: "meths[0]", funcExp: "function(s) { return s; }" }, + { assignTo: "let meths: Method[]; meths", method: "meths[0]", funcExp: "(function(s) { return s; })" }, + { assignTo: "const [meth]: Method[]", method: "meth", funcExp: "s => s" }, + { assignTo: "const [meth]: Method[]", method: "meth", funcExp: "(s => s)" }, + { assignTo: "const [meth]: Method[]", method: "meth", funcExp: "function(s) { return s; }" }, + { assignTo: "const [meth]: Method[]", method: "meth", funcExp: "(function(s) { return s; })" }, + { assignTo: "let meth: Method; [meth]", method: "meth", funcExp: "s => s" }, + { assignTo: "let meth: Method; [meth]", method: "meth", funcExp: "(s => s)" }, + { assignTo: "let meth: Method; [meth]", method: "meth", funcExp: "function(s) { return s; }" }, + { assignTo: "let meth: Method; [meth]", method: "meth", funcExp: "(function(s) { return s; })" }, +])("Function expression type inference in array (%p)", ({ assignTo, method, funcExp }) => { + util.testFunction` + interface Foo { + method(s: string): string; + } + interface Method { + (this: Foo, s: string): string; + } + ${assignTo} = [${funcExp}]; + const foo: Foo = {method: ${method}}; + return foo.method("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { funcType: "(this: void, s: string) => string", funcExp: "s => s" }, + { funcType: "(this: any, s: string) => string", funcExp: "s => s" }, + { funcType: "(s: string) => string", funcExp: "s => s" }, + { funcType: "(this: void, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(this: any, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(s: string) => string", funcExp: "function(s) { return s; }" }, +])("Function expression type inference in union (%p)", ({ funcType, funcExp }) => { + util.testFunction` + type U = string | number | (${funcType}); + const u: U = ${funcExp}; + return (u as ${funcType})("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { funcType: "(this: void, s: string) => string", funcExp: "s => s" }, + { funcType: "(this: any, s: string) => string", funcExp: "s => s" }, + { funcType: "(s: string) => string", funcExp: "s => s" }, + { funcType: "(this: void, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(this: any, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(s: string) => string", funcExp: "function(s) { return s; }" }, +])("Function expression type inference in union tuple (%p)", ({ funcType, funcExp }) => { + util.testFunction` + interface I { callback: ${funcType}; } + let a: I[] | number = [{ callback: ${funcExp} }]; + return a[0].callback("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { funcType: "(this: void, s: string) => string", funcExp: "s => s" }, + { funcType: "(this: any, s: string) => string", funcExp: "s => s" }, + { funcType: "(s: string) => string", funcExp: "s => s" }, + { funcType: "(this: void, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(this: any, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(s: string) => string", funcExp: "function(s) { return s; }" }, +])("Function expression type inference in as cast (%p)", ({ funcType, funcExp }) => { + util.testFunction` + const fn: ${funcType} = (${funcExp}) as (${funcType}); + return fn("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { funcType: "(this: void, s: string) => string", funcExp: "s => s" }, + { funcType: "(this: any, s: string) => string", funcExp: "s => s" }, + { funcType: "(s: string) => string", funcExp: "s => s" }, + { funcType: "(this: void, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(this: any, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(s: string) => string", funcExp: "function(s) { return s; }" }, +])("Function expression type inference in type assertion (%p)", ({ funcType, funcExp }) => { + util.testFunction` + const fn: ${funcType} = <${funcType}>(${funcExp}); + return fn("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { funcType: "(this: void, s: string) => string", funcExp: "s => s" }, + { funcType: "(this: any, s: string) => string", funcExp: "s => s" }, + { funcType: "(s: string) => string", funcExp: "s => s" }, + { funcType: "(this: void, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(this: any, s: string) => string", funcExp: "function(s) { return s; }" }, + { funcType: "(s: string) => string", funcExp: "function(s) { return s; }" }, +])("Function expression type inference in constructor (%p)", ({ funcType, funcExp }) => { + util.testFunction` + class C { + result: string; + constructor(fn: ${funcType}) { this.result = fn("foo"); } + } + const c = new C(${funcExp}); + return c.result; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/functions/validation/functionPermutations.ts b/test/unit/functions/validation/functionPermutations.ts new file mode 100644 index 000000000..14e478498 --- /dev/null +++ b/test/unit/functions/validation/functionPermutations.ts @@ -0,0 +1,439 @@ +export interface TestFunction { + value: string; + definition?: string; +} + +export const selfTestFunctions: TestFunction[] = [ + { + value: "selfFunc", + definition: "let selfFunc: {(this: any, s: string): string} = function(s) { return s; };", + }, + { + value: "selfLambda", + definition: "let selfLambda: (this: any, s: string) => string = s => s;", + }, + { + value: "anonFunc", + definition: "let anonFunc: {(s: string): string} = function(s) { return s; };", + }, + { + value: "anonLambda", + definition: "let anonLambda: (s: string) => string = s => s;", + }, + { + value: "methodClass.method", + definition: `class MethodClass { method(this: any, s: string): string { return s; } } + const methodClass = new MethodClass();`, + }, + { + value: "anonMethodClass.anonMethod", + definition: `class AnonMethodClass { anonMethod(s: string): string { return s; } } + const anonMethodClass = new AnonMethodClass();`, + }, + { + value: "funcPropClass.funcProp", + definition: `class FuncPropClass { funcProp: (this: any, s: string) => string = s => s; } + const funcPropClass = new FuncPropClass();`, + }, + { + value: "anonFuncPropClass.anonFuncProp", + definition: `class AnonFuncPropClass { anonFuncProp: (s: string) => string = s => s; } + const anonFuncPropClass = new AnonFuncPropClass();`, + }, + { + value: "StaticMethodClass.staticMethod", + definition: `class StaticMethodClass { + static staticMethod(this: any, s: string): string { return s; } + }`, + }, + { + value: "AnonStaticMethodClass.anonStaticMethod", + definition: "class AnonStaticMethodClass { static anonStaticMethod(s: string): string { return s; } }", + }, + { + value: "StaticFuncPropClass.staticFuncProp", + definition: `class StaticFuncPropClass { + static staticFuncProp: (this: any, s: string) => string = s => s; + }`, + }, + { + value: "AnonStaticFuncPropClass.anonStaticFuncProp", + definition: `class AnonStaticFuncPropClass { + static anonStaticFuncProp: (s: string) => string = s => s; + }`, + }, + { + value: "FuncNs.nsFunc", + definition: "namespace FuncNs { export function nsFunc(s: string) { return s; } }", + }, + { + value: "FuncNestedNs.NestedNs.nestedNsFunc", + definition: `namespace FuncNestedNs { + export namespace NestedNs { export function nestedNsFunc(s: string) { return s; } } + }`, + }, + { + value: "LambdaNs.nsLambda", + definition: `namespace LambdaNs { + export let nsLambda: (s: string) => string = s => s; + }`, + }, + { + value: "LambdaNestedNs.NestedNs.nestedNsLambda", + definition: `namespace LambdaNestedNs { + export namespace NestedNs { export let nestedNsLambda: (s: string) => string = s => s } + }`, + }, + { + value: "methodInterface.method", + definition: `interface MethodInterface { method(this: any, s: string): string; } + const methodInterface: MethodInterface = { method: function(this: any, s: string): string { return s; } };`, + }, + { + value: "anonMethodInterface.anonMethod", + definition: `interface AnonMethodInterface { anonMethod(s: string): string; } + const anonMethodInterface: AnonMethodInterface = { + anonMethod: function(this: any, s: string): string { return s; } + };`, + }, + { + value: "funcPropInterface.funcProp", + definition: `interface FuncPropInterface { funcProp: (this: any, s: string) => string; } + const funcPropInterface: FuncPropInterface = { funcProp: function(this: any, s: string) { return s; } };`, + }, + { + value: "anonFuncPropInterface.anonFuncProp", + definition: `interface AnonFuncPropInterface { anonFuncProp: (s: string) => string; } + const anonFuncPropInterface: AnonFuncPropInterface = { anonFuncProp: (s: string): string => s };`, + }, + { + value: "anonMethodClassInNoSelfNs.method", + definition: `/** @noSelf */ namespace AnonMethodClassInNoSelfNs { + export class MethodClass { + method(s: string): string { return s; } + } + } + const anonMethodClassInNoSelfNs = new AnonMethodClassInNoSelfNs.MethodClass();`, + }, + { + value: "anonMethodInterfaceInNoSelfNs.method", + definition: `/** @noSelf */ namespace AnonMethodInterfaceInNoSelfNs { + export interface MethodInterface { + method(s: string): string; + } + } + const anonMethodInterfaceInNoSelfNs: AnonMethodInterfaceInNoSelfNs.MethodInterface = { + method: function(s: string): string { return s; } + };`, + }, + { + value: "anonFunctionNestedInNoSelfClass", + definition: `/** @noSelf */ class AnonFunctionNestedInNoSelfClass { + method() { return function(s: string) { return s; } } + } + const anonFunctionNestedInNoSelfClass = (new AnonFunctionNestedInNoSelfClass).method();`, + }, + { + value: "anonMethodClassMergedNoSelfNS.method", + definition: `class AnonMethodClassMergedNoSelfNS { method(s: string): string { return s; } } + /** @noSelf */ namespace AnonMethodClassMergedNoSelfNS { export function nsFunc(s: string) { return s; } } + const anonMethodClassMergedNoSelfNS = new AnonMethodClassMergedNoSelfNS();`, + }, + { + value: "AnonFuncNSMergedNoSelfClass.nsFunc", + definition: `/** @noSelf */ class AnonFuncNSMergedNoSelfClass { method(s: string): string { return s; } } + namespace AnonFuncNSMergedNoSelfClass { export function nsFunc(s: string) { return s; } }`, + }, + { + value: "SelfAnonFuncNSMergedNoSelfNS.nsFuncSelf", + definition: `namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace SelfAnonFuncNSMergedNoSelfNS { export function nsFuncNoSelf(s: string) { return s; } }`, + }, +]; + +export const noSelfTestFunctions: TestFunction[] = [ + { + value: "voidFunc", + definition: "let voidFunc: {(this: void, s: string): string} = function(s) { return s; };", + }, + { + value: "voidLambda", + definition: "let voidLambda: (this: void, s: string) => string = s => s;", + }, + { + value: "voidMethodClass.voidMethod", + definition: `class VoidMethodClass { + voidMethod(this: void, s: string): string { return s; } + } + const voidMethodClass = new VoidMethodClass();`, + }, + { + value: "voidFuncPropClass.voidFuncProp", + definition: `class VoidFuncPropClass { + voidFuncProp: (this: void, s: string) => string = s => s; + } + const voidFuncPropClass = new VoidFuncPropClass();`, + }, + { + value: "StaticVoidMethodClass.staticVoidMethod", + definition: `class StaticVoidMethodClass { + static staticVoidMethod(this: void, s: string): string { return s; } + }`, + }, + { + value: "StaticVoidFuncPropClass.staticVoidFuncProp", + definition: `class StaticVoidFuncPropClass { + static staticVoidFuncProp: (this: void, s: string) => string = s => s; + }`, + }, + { + value: "NoSelfFuncNs.noSelfNsFunc", + definition: "/** @noSelf */ namespace NoSelfFuncNs { export function noSelfNsFunc(s: string) { return s; } }", + }, + { + value: "NoSelfFuncNs.noSelfNsFunc", + definition: `namespace NoSelfFuncNs { + /** @noSelf */ + export function noSelfNsFunc(s: string) { return s; } }`, + }, + { + value: "NoSelfFuncNestedNs.NestedNs.noSelfNestedNsFunc", + definition: `/** @noSelf */ namespace NoSelfFuncNestedNs { + export namespace NestedNs { export function noSelfNestedNsFunc(s: string) { return s; } } + }`, + }, + { + value: "NoSelfLambdaNs.noSelfNsLambda", + definition: `/** @noSelf */ namespace NoSelfLambdaNs { + export let noSelfNsLambda: (s: string) => string = s => s; + }`, + }, + { + value: "NoSelfLambdaNestedNs.NestedNs.noSelfNestedNsLambda", + definition: `/** @noSelf */ namespace NoSelfLambdaNestedNs { + export namespace NestedNs { export let noSelfNestedNsLambda: (s: string) => string = s => s } + }`, + }, + { + value: "noSelfMethodClass.noSelfMethod", + definition: `/** @noSelf */ class NoSelfMethodClass { noSelfMethod(s: string): string { return s; } } + const noSelfMethodClass = new NoSelfMethodClass();`, + }, + { + value: "noSelfMethodClass.noSelfMethod", + definition: `class NoSelfMethodClass { + /** @noSelf */ + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClass = new NoSelfMethodClass();`, + }, + { + value: "NoSelfStaticMethodClass.noSelfStaticMethod", + definition: `/** @noSelf */ class NoSelfStaticMethodClass { + static noSelfStaticMethod(s: string): string { return s; } + }`, + }, + { + value: "noSelfFuncPropClass.noSelfFuncProp", + definition: `/** @noSelf */ class NoSelfFuncPropClass { noSelfFuncProp: (s: string) => string = s => s; } + const noSelfFuncPropClass = new NoSelfFuncPropClass();`, + }, + { + value: "NoSelfStaticFuncPropClass.noSelfStaticFuncProp", + definition: `/** @noSelf */ class NoSelfStaticFuncPropClass { + static noSelfStaticFuncProp: (s: string) => string = s => s; + }`, + }, + { + value: "voidMethodInterface.voidMethod", + definition: `interface VoidMethodInterface { + voidMethod(this: void, s: string): string; + } + const voidMethodInterface: VoidMethodInterface = { + voidMethod(this: void, s: string): string { return s; } + };`, + }, + { + value: "voidFuncPropInterface.voidFuncProp", + definition: `interface VoidFuncPropInterface { + voidFuncProp: (this: void, s: string) => string; + } + const voidFuncPropInterface: VoidFuncPropInterface = { + voidFuncProp: function(this: void, s: string): string { return s; } + };`, + }, + { + value: "noSelfMethodInterface.noSelfMethod", + definition: `/** @noSelf */ interface NoSelfMethodInterface { noSelfMethod(s: string): string; } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };`, + }, + { + value: "noSelfMethodInterface.noSelfMethod", + definition: `interface NoSelfMethodInterface { + /** @noSelf */ + noSelfMethod(s: string): string; + } + const noSelfMethodInterface: NoSelfMethodInterface = { + noSelfMethod: function(s: string): string { return s; } + };`, + }, + { + value: "noSelfFuncPropInterface.noSelfFuncProp", + definition: `/** @noSelf */ interface NoSelfFuncPropInterface { noSelfFuncProp(s: string): string; } + const noSelfFuncPropInterface: NoSelfFuncPropInterface = { + noSelfFuncProp: (s: string): string => s + };`, + }, + { + value: "noSelfMethodClassExpression.noSelfMethod", + definition: `/** @noSelf */ const NoSelfMethodClassExpression = class { + noSelfMethod(s: string): string { return s; } + } + const noSelfMethodClassExpression = new NoSelfMethodClassExpression();`, + }, + { + value: "anonFunctionNestedInClassInNoSelfNs", + definition: `/** @noSelf */ namespace AnonFunctionNestedInClassInNoSelfNs { + export class AnonFunctionNestedInClass { + method() { return function(s: string) { return s; } } + } + } + const anonFunctionNestedInClassInNoSelfNs = + (new AnonFunctionNestedInClassInNoSelfNs.AnonFunctionNestedInClass).method();`, + }, + { + value: "noSelfAnonMethodClassMergedNS.method", + definition: `/** @noSelf */ class NoSelfAnonMethodClassMergedNS { method(s: string): string { return s; } } + namespace NoSelfAnonMethodClassMergedNS { export function nsFunc(s: string) { return s; } } + const noSelfAnonMethodClassMergedNS = new NoSelfAnonMethodClassMergedNS();`, + }, + { + value: "NoSelfAnonFuncNSMergedClass.nsFunc", + definition: `class NoSelfAnonFuncNSMergedClass { method(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedClass { export function nsFunc(s: string) { return s; } }`, + }, + { + value: "NoSelfAnonFuncNSMergedSelfNS.nsFuncNoSelf", + definition: `namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncSelf(s: string): string { return s; } } + /** @noSelf */ namespace NoSelfAnonFuncNSMergedSelfNS { export function nsFuncNoSelf(s: string) { return s; } }`, + }, +]; + +const noSelfInFileTestFunctions: TestFunction[] = [ + { + value: "noSelfInFileFunc", + definition: "/** @noSelfInFile */ let noSelfInFileFunc: {(s: string): string} = function(s) { return s; };", + }, + { + value: "noSelfInFileLambda", + definition: "/** @noSelfInFile */ let noSelfInFileLambda: (s: string) => string = s => s;", + }, + { + value: "NoSelfInFileFuncNs.noSelfInFileNsFunc", + definition: `/** @noSelfInFile */ namespace NoSelfInFileFuncNs { + export function noSelfInFileNsFunc(s: string) { return s; } + }`, + }, + { + value: "NoSelfInFileLambdaNs.noSelfInFileNsLambda", + definition: `/** @noSelfInFile */ namespace NoSelfInFileLambdaNs { + export let noSelfInFileNsLambda: (s: string) => string = s => s; + }`, + }, + { + value: "noSelfInFileFuncNestedInClass", + definition: `/** @noSelfInFile */ class NoSelfInFileFuncNestedInClass { + method() { return function(s: string) { return s; } } + } + const noSelfInFileFuncNestedInClass = (new NoSelfInFileFuncNestedInClass).method();`, + }, +]; + +export const anonTestFunctionExpressions: TestFunction[] = [ + { value: "s => s" }, + { value: "(s => s)" }, + { value: "function(s) { return s; }" }, + { value: "(function(s) { return s; })" }, +]; + +export const selfTestFunctionExpressions: TestFunction[] = [ + { value: "function(this: any, s) { return s; }" }, + { value: "(function(this: any, s) { return s; })" }, +]; + +export const noSelfTestFunctionExpressions: TestFunction[] = [ + { value: "function(this: void, s) { return s; }" }, + { value: "(function(this: void, s) { return s; })" }, +]; + +export const anonTestFunctionType = "(s: string) => string"; +export const selfTestFunctionType = "(this: any, s: string) => string"; +export const noSelfTestFunctionType = "(this: void, s: string) => string"; + +type TestFunctionCast = [ + /* testFunction: */ TestFunction, + /* castedFunction: */ string, + /* isSelfConversion?: */ boolean? +]; +export const validTestMethodCasts: TestFunctionCast[] = [ + [selfTestFunctions[0], `<${anonTestFunctionType}>(${selfTestFunctions[0].value})`], + [selfTestFunctions[0], `(${selfTestFunctions[0].value}) as (${anonTestFunctionType})`], + [selfTestFunctions[0], `<${selfTestFunctionType}>(${selfTestFunctions[0].value})`], + [selfTestFunctions[0], `(${selfTestFunctions[0].value}) as (${selfTestFunctionType})`], + [noSelfTestFunctions[0], `<${noSelfTestFunctionType}>(${noSelfTestFunctions[0].value})`], + [noSelfTestFunctions[0], `(${noSelfTestFunctions[0].value}) as (${noSelfTestFunctionType})`], + [noSelfInFileTestFunctions[0], `<${noSelfTestFunctionType}>(${noSelfInFileTestFunctions[0].value})`], + [noSelfInFileTestFunctions[0], `(${noSelfInFileTestFunctions[0].value}) as (${noSelfTestFunctionType})`], +]; +export const validTestFunctionCasts: TestFunctionCast[] = [ + ...validTestMethodCasts, + [noSelfInFileTestFunctions[0], `<${anonTestFunctionType}>(${noSelfInFileTestFunctions[0].value})`], + [noSelfInFileTestFunctions[0], `(${noSelfInFileTestFunctions[0].value}) as (${anonTestFunctionType})`], +]; +export const invalidTestFunctionCasts: TestFunctionCast[] = [ + [noSelfTestFunctions[0], `<${anonTestFunctionType}>(${noSelfTestFunctions[0].value})`, false], + [noSelfTestFunctions[0], `(${noSelfTestFunctions[0].value}) as (${anonTestFunctionType})`, false], + [noSelfTestFunctions[0], `<${selfTestFunctionType}>(${noSelfTestFunctions[0].value})`, false], + [noSelfTestFunctions[0], `(${noSelfTestFunctions[0].value}) as (${selfTestFunctionType})`, false], + [noSelfInFileTestFunctions[0], `<${selfTestFunctionType}>(${noSelfInFileTestFunctions[0].value})`, false], + [noSelfInFileTestFunctions[0], `(${noSelfInFileTestFunctions[0].value}) as (${selfTestFunctionType})`, false], + [selfTestFunctions[0], `<${noSelfTestFunctionType}>(${selfTestFunctions[0].value})`, true], + [selfTestFunctions[0], `(${selfTestFunctions[0].value}) as (${noSelfTestFunctionType})`, true], +]; + +export type TestFunctionAssignment = [ + /* testFunction: */ TestFunction, + /* functionType: */ string, + /* isSelfConversion?: */ boolean? +]; +export const validTestMethodAssignments: TestFunctionAssignment[] = [ + ...selfTestFunctions.map((f): TestFunctionAssignment => [f, anonTestFunctionType]), + ...selfTestFunctions.map((f): TestFunctionAssignment => [f, selfTestFunctionType]), + ...noSelfTestFunctions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType]), + ...noSelfInFileTestFunctions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType]), + ...anonTestFunctionExpressions.map((f): TestFunctionAssignment => [f, anonTestFunctionType]), + ...anonTestFunctionExpressions.map((f): TestFunctionAssignment => [f, selfTestFunctionType]), + ...anonTestFunctionExpressions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType]), + ...selfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, anonTestFunctionType]), + ...selfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, selfTestFunctionType]), + ...noSelfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType]), +]; +export const validTestFunctionAssignments: TestFunctionAssignment[] = [ + ...validTestMethodAssignments, + ...noSelfInFileTestFunctions.map((f): TestFunctionAssignment => [f, anonTestFunctionType]), +]; +export const invalidTestFunctionAssignments: TestFunctionAssignment[] = [ + ...selfTestFunctions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType, false]), + ...noSelfTestFunctions.map((f): TestFunctionAssignment => [f, anonTestFunctionType, true]), + ...noSelfTestFunctions.map((f): TestFunctionAssignment => [f, selfTestFunctionType, true]), + ...noSelfInFileTestFunctions.map((f): TestFunctionAssignment => [f, selfTestFunctionType, true]), + ...selfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType, false]), + ...noSelfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, anonTestFunctionType, true]), + ...noSelfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, selfTestFunctionType, true]), +]; +export const invalidTestMethodAssignments: TestFunctionAssignment[] = [ + ...invalidTestFunctionAssignments, + ...noSelfInFileTestFunctions.map((f): TestFunctionAssignment => [f, anonTestFunctionType, true]), +]; diff --git a/test/unit/functions/validation/invalidFunctionAssignments.spec.ts b/test/unit/functions/validation/invalidFunctionAssignments.spec.ts new file mode 100644 index 000000000..95b26b7d0 --- /dev/null +++ b/test/unit/functions/validation/invalidFunctionAssignments.spec.ts @@ -0,0 +1,243 @@ +import { + unsupportedOverloadAssignment, + unsupportedNoSelfFunctionConversion, + unsupportedSelfFunctionConversion, +} from "../../../../src/transformation/utils/diagnostics"; +import * as util from "../../../util"; +import { + invalidTestFunctionAssignments, + invalidTestFunctionCasts, + invalidTestMethodAssignments, +} from "./functionPermutations"; + +test.each(invalidTestFunctionAssignments)( + "Invalid function variable declaration (%p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + const fn: ${functionType} = ${testFunction.value}; + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestMethodAssignments)( + "Invalid object with method variable declaration (%p, %p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + const obj: { fn: ${functionType} } = {fn: ${testFunction.value}}; + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestFunctionAssignments)( + "Invalid function assignment (%p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + let fn: ${functionType}; + fn = ${testFunction.value}; + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestMethodAssignments)( + "Invalid object with method assignment (%p, %p, %p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + let obj: { fn: ${functionType} }; + obj = {fn: ${testFunction.value}}; + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestFunctionCasts)("Invalid function assignment with cast (%p)", (testFunction, castedFunction) => { + util.testModule` + ${testFunction.definition ?? ""} + let fn: typeof ${testFunction.value}; + fn = ${castedFunction}; + `.expectDiagnosticsToMatchSnapshot( + [unsupportedNoSelfFunctionConversion.code, unsupportedSelfFunctionConversion.code], + true + ); +}); + +test.each(invalidTestFunctionCasts)( + "Invalid object with method assignment with cast (%p, %p, %p)", + (testFunction, castedFunction, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + let obj: { fn: typeof ${testFunction.value} }; + obj = {fn: ${castedFunction}}; + `.expectDiagnosticsToMatchSnapshot( + isSelfConversion + ? [unsupportedSelfFunctionConversion.code, unsupportedNoSelfFunctionConversion.code] + : [unsupportedNoSelfFunctionConversion.code, unsupportedSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestFunctionAssignments)( + "Invalid function argument (%p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + declare function takesFunction(fn: ${functionType}); + takesFunction(${testFunction.value}); + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestMethodAssignments)( + "Invalid object with method argument (%p, %p, %p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + declare function takesObjectWithMethod(obj: { fn: ${functionType} }); + takesObjectWithMethod({fn: ${testFunction.value}}); + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test("Invalid lua lib function argument", () => { + util.testModule` + declare function foo(this: void, value: string): void; + declare const a: string[]; + a.forEach(foo); + `.expectDiagnosticsToMatchSnapshot([unsupportedSelfFunctionConversion.code], true); +}); + +test.each(invalidTestFunctionCasts)("Invalid function argument with cast (%p)", (testFunction, castedFunction) => { + util.testModule` + ${testFunction.definition ?? ""} + declare function takesFunction(fn: typeof ${testFunction.value}); + takesFunction(${castedFunction}); + `.expectDiagnosticsToMatchSnapshot( + [unsupportedNoSelfFunctionConversion.code, unsupportedSelfFunctionConversion.code], + true + ); +}); + +test.each(invalidTestFunctionAssignments)( + "Invalid function generic argument (%p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + declare function takesFunction(fn: T); + takesFunction(${testFunction.value}); + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestFunctionAssignments)( + "Invalid function return (%p)", + (testFunction, functionType, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + function returnsFunction(): ${functionType} { + return ${testFunction.value}; + } + `.expectDiagnosticsToMatchSnapshot( + [isSelfConversion ? unsupportedSelfFunctionConversion.code : unsupportedNoSelfFunctionConversion.code], + true + ); + } +); + +test.each(invalidTestFunctionCasts)( + "Invalid function return with cast (%p)", + (testFunction, castedFunction, isSelfConversion) => { + util.testModule` + ${testFunction.definition ?? ""} + function returnsFunction(): typeof ${testFunction.value} { + return ${castedFunction}; + } + `.expectDiagnosticsToMatchSnapshot( + isSelfConversion + ? [unsupportedSelfFunctionConversion.code, unsupportedNoSelfFunctionConversion.code] + : [unsupportedNoSelfFunctionConversion.code, unsupportedSelfFunctionConversion.code], + true + ); + } +); + +test("Invalid function tuple assignment", () => { + util.testModule` + interface Func { (this: void, s: string): string; } + interface Meth { (this: {}, s: string): string; } + declare function getTuple(): [number, Meth]; + let [i, f]: [number, Func] = getTuple(); + `.expectDiagnosticsToMatchSnapshot([2322, unsupportedNoSelfFunctionConversion.code], true); +}); + +test("Invalid method tuple assignment", () => { + util.testModule` + interface Func { (this: void, s: string): string; } + interface Meth { (this: {}, s: string): string; } + declare function getTuple(): [number, Func]; + let [i, f]: [number, Meth] = getTuple(); + `.expectDiagnosticsToMatchSnapshot([unsupportedSelfFunctionConversion.code], true); +}); + +test("Invalid interface method assignment", () => { + util.testModule` + interface A { fn(s: string): string; } + interface B { fn(this: void, s: string): string; } + declare const a: A; + const b: B = a; + `.expectDiagnosticsToMatchSnapshot([unsupportedNoSelfFunctionConversion.code], true); +}); + +test.each([ + "(this: void, s: string) => string", + "(this: void, s1: string, s2: string) => string", + "{(this: void, s: string): string}", + "{(this: any, s1: string, s2: string): string}", +])("Invalid function overload assignment (%p)", assignType => { + util.testModule` + interface O { + (this: any, s1: string, s2: string): string; + (this: void, s: string): string; + } + declare const o: O; + let f: ${assignType} = o; + `.expectDiagnosticsToMatchSnapshot([unsupportedOverloadAssignment.code], true); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/896 +test("Does not fail on union type signatures (#896)", () => { + util.testExpression`foo<'a'>(() => {});` + .setTsHeader( + ` + declare interface Events { + a(): void; + [key: string]: (this: void) => void; + } + declare function foo(callback: Events[T]): void; + ` + ) + .expectToHaveDiagnostics([unsupportedOverloadAssignment.code]); +}); diff --git a/test/unit/functions/validation/validFunctionAssignments.spec.ts b/test/unit/functions/validation/validFunctionAssignments.spec.ts new file mode 100644 index 000000000..525b95929 --- /dev/null +++ b/test/unit/functions/validation/validFunctionAssignments.spec.ts @@ -0,0 +1,267 @@ +import * as util from "../../../util"; +import { + anonTestFunctionExpressions, + anonTestFunctionType, + noSelfTestFunctionExpressions, + noSelfTestFunctions, + noSelfTestFunctionType, + selfTestFunctionExpressions, + selfTestFunctions, + selfTestFunctionType, + TestFunction, + TestFunctionAssignment, + validTestFunctionAssignments, + validTestFunctionCasts, + validTestMethodAssignments, + validTestMethodCasts, +} from "./functionPermutations"; + +test.each(validTestFunctionAssignments)("Valid function variable declaration (%p)", (testFunction, functionType) => { + util.testFunction` + const fn: ${functionType} = ${testFunction.value}; + return fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each(validTestMethodAssignments)("Valid object wit method declaration (%p, %p)", (testFunction, functionType) => { + util.testFunction` + const obj: { fn: ${functionType} } = {fn: ${testFunction.value}}; + return obj.fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each(validTestFunctionAssignments)("Valid function assignment (%p)", (testFunction, functionType) => { + util.testFunction` + let fn: ${functionType}; + fn = ${testFunction.value}; + return fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each(validTestFunctionCasts)("Valid function assignment with cast (%p)", (testFunction, castedFunction) => { + util.testFunction` + let fn: typeof ${testFunction.value}; + fn = ${castedFunction}; + return fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each(validTestMethodCasts)( + "Valid object with method assignment with cast (%p, %p)", + (testFunction, castedFunction) => { + util.testFunction` + let obj: { fn: typeof ${testFunction.value} }; + obj = {fn: ${castedFunction}}; + return obj.fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); + } +); + +test.each(validTestFunctionAssignments)("Valid function argument (%p)", (testFunction, functionType) => { + util.testFunction` + function takesFunction(fn: ${functionType}) { + return fn("foobar"); + } + return takesFunction(${testFunction.value}); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each(validTestMethodAssignments)("Valid object with method argument (%p, %p)", (testFunction, functionType) => { + util.testFunction` + function takesObjectWithMethod(obj: { fn: ${functionType} }) { + return obj.fn("foobar"); + } + return takesObjectWithMethod({fn: ${testFunction.value}}); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test("Valid lua lib function argument", () => { + util.testFunction` + let result = ""; + function foo(this: any, value: string) { result += value; } + const a = ['foo', 'bar']; + a.forEach(foo); + return result; + `.expectToMatchJsResult(); +}); + +test.each(validTestFunctionCasts)("Valid function argument with cast (%p)", (testFunction, castedFunction) => { + util.testFunction` + function takesFunction(fn: typeof ${testFunction.value}) { + return fn("foobar"); + } + return takesFunction(${castedFunction}); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each([ + // TODO: Fix function expression inference with generic types. The following should work, but doesn't: + // function takesFunction string>(fn: T) { ... } + // takesFunction(s => s); // Error: cannot convert method to function + // ...validTestFunctionAssignments - Use this instead of other cases when fixed + ...selfTestFunctions.map((f): TestFunctionAssignment => [f, anonTestFunctionType]), + ...selfTestFunctions.map((f): TestFunctionAssignment => [f, selfTestFunctionType]), + ...noSelfTestFunctions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType]), + ...selfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, anonTestFunctionType]), + ...selfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, selfTestFunctionType]), + ...noSelfTestFunctionExpressions.map((f): TestFunctionAssignment => [f, noSelfTestFunctionType]), +])("Valid function generic argument (%p)", (testFunction, functionType) => { + util.testFunction` + function takesFunction(fn: T) { + return fn("foobar"); + } + return takesFunction(${testFunction.value}); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each([ + ...anonTestFunctionExpressions.map((f): [TestFunction, string[]] => [f, ["0", "'foobar'"]]), + ...selfTestFunctionExpressions.map((f): [TestFunction, string[]] => [f, ["0", "'foobar'"]]), + ...noSelfTestFunctionExpressions.map((f): [TestFunction, string[]] => [f, ["'foobar'"]]), +])("Valid function expression argument with no signature (%p, %p)", (testFunction, args) => { + util.testFunction` + const takesFunction: any = (fn: (this: void, ...args: any[]) => any, ...args: any[]) => { + return fn(...args); + } + return takesFunction(${testFunction.value}, ${args.join(", ")}); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToEqual("foobar"); +}); + +test.each(validTestFunctionAssignments)("Valid function return (%p)", (testFunction, functionType) => { + util.testFunction` + function returnsFunction(): ${functionType} { + return ${testFunction.value}; + } + const fn = returnsFunction(); + return fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test.each(validTestFunctionCasts)("Valid function return with cast (%p)", (testFunction, castedFunction) => { + util.testFunction` + function returnsFunction(): typeof ${testFunction.value} { + return ${castedFunction}; + } + const fn = returnsFunction(); + return fn("foobar"); + ` + .setTsHeader(testFunction.definition ?? "") + .expectToMatchJsResult(); +}); + +test("Valid function tuple assignment", () => { + util.testFunction` + interface Func { (this: void, s: string): string; } + function getTuple(): [number, Func] { return [1, s => s]; } + let [i, f]: [number, Func] = getTuple(); + return f("foo"); + `.expectToMatchJsResult(); +}); + +test("Interface method assignment", () => { + util.testFunction` + class Foo { + method(s: string): string { return s + "+method"; } + lambdaProp: (s: string) => string = s => s + "+lambdaProp"; + } + interface IFoo { + method: (s: string) => string; + lambdaProp(s: string): string; + } + const foo: IFoo = new Foo(); + return foo.method("foo") + "|" + foo.lambdaProp("bar"); + `.expectToMatchJsResult(); +}); + +test("Valid interface method assignment", () => { + util.testFunction` + interface A { fn(this: void, s: string): string; } + interface B { fn(this: void, s: string): string; } + const a: A = { fn(this: void, s) { return s; } }; + const b: B = a; + return b.fn("foo"); + `.expectToMatchJsResult(); +}); + +test("Valid method tuple assignment", () => { + util.testFunction` + interface Foo { method(s: string): string; } + interface Meth { (this: Foo, s: string): string; } + let meth: Meth = s => s; + function getTuple(): [number, Meth] { return [1, meth]; } + let [i, f]: [number, Meth] = getTuple(); + let foo: Foo = {method: f}; + return foo.method("foo"); + `.expectToMatchJsResult(); +}); + +test.each([ + { assignType: "(this: any, s: string) => string", args: ["foo"] }, + { assignType: "{(this: any, s: string): string}", args: ["foo"] }, + { assignType: "(this: any, s1: string, s2: string) => string", args: ["foo", "baz"] }, + { assignType: "{(this: any, s1: string, s2: string): string}", args: ["foo", "baz"] }, +])("Valid function overload assignment (%p)", ({ assignType, args }) => { + util.testFunction` + interface O { + (s1: string, s2: string): string; + (s: string): string; + } + const o: O = (s1: string, s2?: string) => s1 + (s2 || "bar"); + let f: ${assignType} = o; + return f(${args.map(a => '"' + a + '"').join(", ")}); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/896 +test("Does not fail on union type signatures (#896)", () => { + util.testExpression`foo<'a'>(() => {});` + .setTsHeader( + ` + declare interface Events { + a(): void; + [key: string]: Function; + } + declare function foo(callback: Events[T]): void; + ` + ) + .expectToHaveNoDiagnostics(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1568 +test("No false positives when using generic functions (#1568)", () => { + util.testModule` + /** @noSelf */ + declare namespace Test { + export function testCallback void>( + callback: T, + ): void; + } + + Test.testCallback(() => {}); + + const f = () => {}; + Test.testCallback(f); + `.expectToHaveNoDiagnostics(); +}); diff --git a/test/unit/hoisting.spec.ts b/test/unit/hoisting.spec.ts index 44ed9d0dd..144460067 100644 --- a/test/unit/hoisting.spec.ts +++ b/test/unit/hoisting.spec.ts @@ -1,257 +1,260 @@ import * as ts from "typescript"; -import { Expect, Test, TestCase } from "alsatian"; +import * as util from "../util"; -import * as util from "../src/util"; -import { CompilerOptions, LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; -import { TranspileError } from "../../src/TranspileError"; +test.each(["let", "const"])("Let/Const Hoisting (%p)", varType => { + util.testFunction` + let bar: string; + function setBar() { bar = foo; } + ${varType} foo = "foo"; + setBar(); + return foo; + `.expectToMatchJsResult(); +}); -export class HoistingTests { - @Test("Var Hoisting") - public varHoisting(): void { - const code = - `foo = "foo"; - var foo; - return foo;`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } +test.each(["let", "const"])("Exported Let/Const Hoisting (%p)", varType => { + util.testModule` + let bar: string; + function setBar() { bar = foo; } + export ${varType} foo = "foo"; + setBar(); + `.expectToMatchJsResult(); +}); - @Test("Exported Var Hoisting") - public exportedVarHoisting(): void { - const code = - `foo = "foo"; - export var foo;`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } +test("Global Function Hoisting", () => { + util.testFunction` + const foo = bar(); + function bar() { return "bar"; } + return foo; + `.expectToMatchJsResult(); +}); - @TestCase("let") - @TestCase("const") - @Test("Let/Const Hoisting") - public letConstHoisting(varType: string): void { - const code = - `let bar: string; - function setBar() { bar = foo; } - ${varType} foo = "foo"; - setBar(); - return foo;`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } +test("Local Function Hoisting", () => { + util.testModule` + export const foo = bar(); + function bar() { return "bar"; } + `.expectToMatchJsResult(); +}); - @TestCase("let") - @TestCase("const") - @Test("Exported Let/Const Hoisting") - public exportedLetConstHoisting(varType: string): void { - const code = - `let bar: string; - function setBar() { bar = foo; } - export ${varType} foo = "foo"; - setBar();`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } +test("Exported Function Hoisting", () => { + util.testModule` + const foo = bar(); + export function bar() { return "bar"; } + export const baz = foo; + ` + .setReturnExport("baz") + .expectToMatchJsResult(); +}); - @Test("Global Function Hoisting") - public globalFunctionHoisting(): void { - const code = - `const foo = bar(); - function bar() { return "bar"; } - return foo;`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("bar"); - } - - @Test("Local Function Hoisting") - public localFunctionHoisting(): void { - const code = - `export const foo = bar(); - function bar() { return "bar"; }`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("bar"); - } - - @Test("Exported Function Hoisting") - public exportedFunctionHoisting(): void { - const code = - `const foo = bar(); - export function bar() { return "bar"; } - export const baz = foo;`; - const result = util.transpileExecuteAndReturnExport(code, "baz"); - Expect(result).toBe("bar"); - } - - @Test("Namespace Function Hoisting") - public namespaceFunctionHoisting(): void { - const code = - `let foo: string; +test("Namespace Function Hoisting", () => { + util.testFunction` + return foo; + ` + .setTsHeader( + ` + let foo: string; namespace NS { foo = bar(); function bar() { return "bar"; } - }`; - const result = util.transpileAndExecute("return foo;", undefined, undefined, code); - Expect(result).toBe("bar"); - } + } + ` + ) + .expectToMatchJsResult(); +}); - @Test("Exported Namespace Function Hoisting") - public exportedNamespaceFunctionHoisting(): void { - const code = - `let foo: string; +test("Exported Namespace Function Hoisting", () => { + util.testFunction("return foo;") + .setTsHeader( + ` + let foo: string; namespace NS { foo = bar(); export function bar() { return "bar"; } - }`; - const result = util.transpileAndExecute("return foo;", undefined, undefined, code); - Expect(result).toBe("bar"); - } - - @TestCase("var", "foo") - @TestCase("let", "bar") - @TestCase("const", "bar") - @Test("Hoisting in Non-Function Scope") - public hoistingInNonFunctionScope(varType: string, expectResult: string): void { - const code = - `function foo() { - ${varType} bar = "bar"; - for (let i = 0; i < 1; ++i) { - ${varType} bar = "foo"; - } - return bar; } - return foo();`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } + ` + ) + .expectToMatchJsResult(); +}); - @TestCase("", "foofoo") - @TestCase(" = \"bar\"", "barbar") - @Test("Var hoisting from child scope") - public varHoistingFromChildScope(initializer: string, expectResult: string): void { - const code = - `foo = "foo"; - let result: string; - if (true) { - var foo${initializer}; - result = foo; +test.each(["let", "const"])("Hoisting in Non-Function Scope (%p)", varType => { + util.testFunction` + function foo() { + ${varType} bar = "bar"; + for (let i = 0; i < 1; ++i) { + ${varType} bar = "foo"; } - return foo + result;`; - const result = util.transpileAndExecute(code); - Expect(result).toBe(expectResult); - } + return bar; + } + return foo(); + `.expectToMatchJsResult(); +}); - @Test("Hoisting due to reference from hoisted function") - public hoistingDueToReferenceFromHoistedFunction(): void { - const code = - `const foo = "foo"; - const result = bar(); - function bar() { - return foo; - } - return result;`; - const result = util.transpileAndExecute(code); - Expect(result).toBe("foo"); - } +test("Hoisting due to reference from hoisted function", () => { + util.testFunction` + const foo = "foo"; + const result = bar(); + function bar() { + return foo; + } + return result; + `.expectToMatchJsResult(); +}); - @Test("Namespace Hoisting") - public namespaceHoisting(): void { - const code = - `function bar() { - return NS.foo; - } - namespace NS { - export let foo = "foo"; - } - export const foo = bar();`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } +test("Hoisting with synthetic source file node", () => { + util.testModule` + export const foo = bar(); + function bar() { return "bar"; } + ` + .setCustomTransformers({ + before: [ + () => sourceFile => + ts.factory.updateSourceFile( + sourceFile, + [ts.factory.createNotEmittedStatement(undefined!), ...sourceFile.statements], + sourceFile.isDeclarationFile, + sourceFile.referencedFiles, + sourceFile.typeReferenceDirectives, + sourceFile.hasNoDefaultLib, + sourceFile.libReferenceDirectives + ), + ], + }) + .expectToMatchJsResult(); +}); - @Test("Exported Namespace Hoisting") - public exportedNamespaceHoisting(): void { - const code = - `function bar() { - return NS.foo; - } - export namespace NS { - export let foo = "foo"; - } - export const foo = bar();`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } +test("Namespace Hoisting", () => { + util.testModule` + function bar() { + return NS.foo; + } + namespace NS { + export let foo = "foo"; + } + export const foo = bar(); + `.expectToMatchJsResult(); +}); - @Test("Nested Namespace Hoisting") - public nestedNamespaceHoisting(): void { - const code = - `export namespace Outer { - export function bar() { - return Inner.foo; - } - namespace Inner { - export let foo = "foo"; - } - } - export const foo = Outer.bar();`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } +test("Exported Namespace Hoisting", () => { + util.testModule` + function bar() { + return NS.foo; + } + export namespace NS { + export let foo = "foo"; + } + export const foo = bar(); + `.expectToMatchJsResult(); +}); - @Test("Class Hoisting") - public classHoisting(): void { - const code = - `function makeFoo() { - return new Foo(); +test("Nested Namespace Hoisting", () => { + util.testModule` + const Inner = 0; + namespace Outer { + export function bar() { + return Inner.foo; } - class Foo { - public bar = "foo"; + namespace Inner { + export const foo = "foo"; } - export const foo = makeFoo().bar;`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } + } - @Test("Enum Hoisting") - public enumHoisting(): void { - const code = - `function bar() { - return E.A; - } - enum E { - A = "foo" - } - export const foo = bar();`; - const result = util.transpileExecuteAndReturnExport(code, "foo"); - Expect(result).toBe("foo"); - } + export const foo = Outer.bar(); + export { Inner }; + `.expectToMatchJsResult(); +}); + +test("Class Hoisting", () => { + util.testModule` + function makeFoo() { + return new Foo(); + } + class Foo { + public bar = "foo"; + } + export const foo = makeFoo().bar; + `.expectToMatchJsResult(); +}); + +test("Enum Hoisting", () => { + util.testModule` + function bar() { + return E.A; + } + enum E { + A = "foo" + } + export const foo = bar(); + `.expectToHaveNoDiagnostics(); +}); + +test("Import hoisting (named)", () => { + // TODO cant be tested with expectToEqualJSResult because of + // the scuffed module setup in TestBuilder.executeJs (module hoisting is not possible) + // should be updated once vm.module becomes stable + util.testModule` + export const result = foo; + import { foo } from "./module"; + ` + .addExtraFile("module.ts", "export const foo = true;") + .expectToEqual({ result: true }); +}); + +test("Import hoisting (namespace)", () => { + // TODO cant be tested with expectToEqualJSresult because of + // the scuffed module setup in TestBuilder.executeJs (module hoisting is not possible) + // should be updated once vm.module becomes stable + util.testModule` + export const result = m.foo; + import * as m from "./module"; + ` + .addExtraFile("module.ts", "export const foo = true;") + .expectToEqual({ result: true }); +}); + +test("Import hoisting (side-effect)", () => { + // TODO cant be tested with expectToEqualJSResult because of + // the scuffed module setup in TestBuilder.executeJs (module hoisting is not possible) + // should be updated once vm.module becomes stable + util.testModule` + export const result = (globalThis as any).result; + import "./module"; + ` + .addExtraFile("module.ts", "(globalThis as any).result = true; export {};") + .expectToEqual({ result: true }); +}); + +test("Import hoisted before function", () => { + // Can't use expectToMatchJsResult because above is not valid TS/JS + util.testModule` + export let result: any; + + baz(); + function baz() { + result = foo; + } + + import { foo } from "./module"; + ` + .addExtraFile("module.ts", "export const foo = true;") + .expectToEqual({ result: true }); +}); + +test("Hoisting Shorthand Property", () => { + util.testFunction` + function foo() { + return { bar }.bar; + } + let bar = "foobar"; + return foo(); + `.expectToMatchJsResult(); +}); - @TestCase(`foo = "foo"; var foo;`, "foo") - @TestCase(`foo = "foo"; export var foo;`, "foo") - @TestCase(`function setBar() { const bar = foo; } let foo = "foo";`, "foo") - @TestCase(`function setBar() { const bar = foo; } const foo = "foo";`, "foo") - @TestCase(`function setBar() { const bar = foo; } export let foo = "foo";`, "foo") - @TestCase(`function setBar() { const bar = foo; } export const foo = "foo";`, "foo") - @TestCase(`const foo = bar(); function bar() { return "bar"; }`, "bar") - @TestCase(`export const foo = bar(); function bar() { return "bar"; }`, "bar") - @TestCase(`const foo = bar(); export function bar() { return "bar"; }`, "bar") - @TestCase(`function bar() { return NS.foo; } namespace NS { export let foo = "foo"; }`, "NS") - @TestCase( - `export namespace O { export function f() { return I.foo; } namespace I { export let foo = "foo"; } }`, - "I" - ) - @TestCase(`function makeFoo() { return new Foo(); } class Foo {}`, "Foo") - @TestCase(`function bar() { return E.A; } enum E { A = "foo" }`, "E") - @Test("No Hoisting") - public noHoisting(code: string, identifier: string): void { - const compilerOptions: CompilerOptions = { - noHoisting: true, - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - Expect(() => util.transpileString(code, compilerOptions)).toThrowError( - TranspileError, - `Identifier "${identifier}" was referenced before it was declared. The declaration ` + - "must be moved before the identifier's use, or hoisting must be enabled." - ); - } -} +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/944 +test("Hoisting variable without initializer", () => { + util.testFunction` + function foo() { + return x; + } + let x: number; + return foo(); + `.expectToMatchJsResult(); +}); diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts new file mode 100644 index 000000000..f148c3c72 --- /dev/null +++ b/test/unit/identifiers.spec.ts @@ -0,0 +1,1057 @@ +import { LuaTarget } from "../../src"; +import { invalidAmbientIdentifierName } from "../../src/transformation/utils/diagnostics"; +import { luaKeywords } from "../../src/transformation/utils/safe-names"; +import * as util from "../util"; + +const invalidLuaCharNames = ["$$$", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"]; +const validTsInvalidLuaKeywordNames = [ + "and", + "elseif", + "end", + "goto", + "local", + "nil", + "not", + "or", + "repeat", + "then", + "until", +]; +const invalidLuaNames = [...invalidLuaCharNames, ...luaKeywords]; +const validTsInvalidLuaNames = [...invalidLuaCharNames, ...validTsInvalidLuaKeywordNames]; + +test.each(validTsInvalidLuaNames)("invalid lua identifier name (%p)", name => { + util.testFunction` + const ${name} = "foobar"; + return ${name}; + `.expectToMatchJsResult(); +}); + +test.each([...luaKeywords.values()])("lua keyword as property name (%p)", keyword => { + util.testFunction` + const x = { ${keyword}: "foobar" }; + return x.${keyword}; + `.expectToMatchJsResult(); +}); + +test.each(validTsInvalidLuaKeywordNames)("destructuring lua keyword (%p)", keyword => { + util.testFunction` + const { foo: ${keyword} } = { foo: "foobar" }; + return ${keyword}; + `.expectToMatchJsResult(); +}); + +test.each(validTsInvalidLuaKeywordNames)("destructuring shorthand lua keyword (%p)", keyword => { + util.testFunction` + const { ${keyword} } = { ${keyword}: "foobar" }; + return ${keyword}; + `.expectToMatchJsResult(); +}); + +test.each(invalidLuaNames)("lua keyword or invalid identifier as method call (%p)", name => { + util.testFunction` + const foo = { + ${name}(arg: string) { return "foo" + arg; } + }; + return foo.${name}("bar"); + `.expectToMatchJsResult(); +}); + +test.each(invalidLuaNames)("lua keyword or invalid identifier as complex method call (%p)", name => { + util.testFunction` + const foo = { + ${name}(arg: string) { return "foo" + arg; } + }; + function getFoo() { return foo; } + return getFoo().${name}("bar"); + `.expectToMatchJsResult(); +}); + +test.each([ + "var local: any;", + "let local: any;", + "const local: any;", + "const foo: any, bar: any, local: any;", + "class local {}", + "namespace local { export const bar: any; }", + "module local { export const bar: any; }", + "enum local {}", + "function local() {}", +])("ambient identifier cannot be a lua keyword (%p)", statement => { + util.testModule` + declare ${statement} + local; + ` + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([invalidAmbientIdentifierName.code]); +}); + +test.each([ + "var $$$: any;", + "let $$$: any;", + "const $$$: any;", + "const foo: any, bar: any, $$$: any;", + "class $$$ {}", + "namespace $$$ { export const bar: any; }", + "module $$$ { export const bar: any; }", + "enum $$$ {}", + "function $$$();", +])("ambient identifier must be a valid lua identifier (%p)", statement => { + util.testModule` + declare ${statement} + $$$; + `.expectDiagnosticsToMatchSnapshot([invalidAmbientIdentifierName.code]); +}); + +test.each(validTsInvalidLuaNames)( + "ambient identifier must be a valid lua identifier (object literal shorthand) (%p)", + name => { + util.testModule` + declare var ${name}: any; + const foo = { ${name} }; + `.expectDiagnosticsToMatchSnapshot([invalidAmbientIdentifierName.code]); + } +); + +test.each(validTsInvalidLuaNames)("undeclared identifier must be a valid lua identifier (%p)", name => { + util.testModule` + const foo = ${name}; + ` + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([invalidAmbientIdentifierName.code]); +}); + +test.each(validTsInvalidLuaNames)( + "undeclared identifier must be a valid lua identifier (object literal shorthand) (%p)", + name => { + util.testModule` + const foo = { ${name} }; + ` + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([invalidAmbientIdentifierName.code]); + } +); + +test.each(validTsInvalidLuaNames)("exported values with invalid lua identifier names (%p)", name => { + const testBuilder = util.testModule(`export const ${name} = "foobar";`); + const lua = testBuilder.getMainLuaCodeChunk(); + const luaResult = testBuilder.getLuaExecutionResult(); + expect(lua.indexOf(`"${name}"`)).toBeGreaterThanOrEqual(0); + expect(luaResult[name]).toBe("foobar"); +}); + +test("exported identifiers referenced in namespace (%p)", () => { + util.testModule` + export const foo = "foobar"; + namespace NS { + export const bar = foo; + } + export const baz = NS.bar; + `.expectToMatchJsResult(); +}); + +test("exported namespace identifiers referenced in different namespace (%p)", () => { + const tsHeader = ` + namespace A { + export const foo = "foobar"; + namespace B { + export const bar = foo; + } + export const baz = B.bar; + }`; + util.testFunction("return A.baz").setTsHeader(tsHeader).expectToMatchJsResult(); +}); + +test("exported identifiers referenced in nested scope (%p)", () => { + util.testModule` + export const foo = "foobar"; + namespace A { + export namespace B { + export const bar = foo; + } + } + export const baz = A.B.bar; + `.expectToMatchJsResult(); +}); + +test.each(validTsInvalidLuaNames)( + "exported values with invalid lua identifier names referenced in different scope (%p)", + name => { + util.testModule` + export const ${name} = "foobar"; + namespace NS { + export const foo = ${name}; + } + export const bar = NS.foo; + `.expectToMatchJsResult(); + } +); + +test.each(validTsInvalidLuaNames)("class with invalid lua name has correct name property", name => { + util.testFunction` + class ${name} {} + return ${name}.name; + `.expectToMatchJsResult(); +}); + +test.each(validTsInvalidLuaNames)("decorated class with invalid lua name", name => { + util.testFunction` + function decorator any>(Class: T, context: ClassDecoratorContext): T { + return class extends Class { + public bar = "foobar"; + }; + } + + @decorator + class ${name} {} + return (${name} as any).bar; + `.expectToMatchJsResult(); +}); + +test.each(validTsInvalidLuaNames)("exported decorated class with invalid lua name", name => { + util.testModule` + function decorator any>(Class: T, context: ClassDecoratorContext): T { + return class extends Class { + public bar = "foobar"; + }; + } + + @decorator + export class ${name} {} + ` + .setReturnExport(name, "bar") + .expectToMatchJsResult(); +}); + +describe("unicode identifiers in supporting environments (luajit)", () => { + const unicodeNames = ["𝛼𝛽𝚫", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"]; + + test.each(unicodeNames)("identifier name (%p)", name => { + util.testFunction` + const ${name} = "foobar"; + return ${name}; + ` + .setOptions({ luaTarget: LuaTarget.LuaJIT }) + .expectLuaToMatchSnapshot(); + }); + + test.each(unicodeNames)("property name (%p)", name => { + util.testFunction` + const x = { ${name}: "foobar" }; + return x.${name}; + ` + .setOptions({ luaTarget: LuaTarget.LuaJIT }) + .expectLuaToMatchSnapshot(); + }); + + test.each(unicodeNames)("destructuring property name (%p)", name => { + util.testFunction` + const { foo: ${name} } = { foo: "foobar" }; + return ${name}; + ` + .setOptions({ luaTarget: LuaTarget.LuaJIT }) + .expectLuaToMatchSnapshot(); + }); + + test.each(unicodeNames)("destructuring shorthand (%p)", name => { + util.testFunction` + const { ${name} } = { ${name}: "foobar" }; + return ${name}; + ` + .setOptions({ luaTarget: LuaTarget.LuaJIT }) + .expectLuaToMatchSnapshot(); + }); + + test.each(unicodeNames)("function (%p)", name => { + util.testFunction` + function ${name}(arg: string) { + return "foo" + arg; + } + return ${name}("bar"); + ` + .setOptions({ luaTarget: LuaTarget.LuaJIT }) + .expectLuaToMatchSnapshot(); + }); + + test.each(unicodeNames)("method (%p)", name => { + util.testFunction` + const foo = { + ${name}(arg: string) { return "foo" + arg; } + }; + return foo.${name}("bar"); + ` + .setOptions({ luaTarget: LuaTarget.LuaJIT }) + .expectLuaToMatchSnapshot(); + }); +}); + +test("unicode export class", () => { + util.testModule` + import { 你好 } from "./utfclass"; + export const result = new 你好().hello(); + ` + .addExtraFile( + "utfclass.ts", + `export class 你好 { + hello() { + return "你好"; + } + }` + ) + .expectToEqual({ result: "你好" }); +}); + +test("unicode export default class", () => { + util.testModule` + import c from "./utfclass"; + export const result = new c().hello(); + ` + .addExtraFile( + "utfclass.ts", + `export default class 你好 { + hello() { + return "你好"; + } + }` + ) + .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` + const nil = "foobar"; + return \`\${undefined}|\${nil}\` + `.expectToEqual("nil|foobar"); + }); + + test("variable (and)", () => { + util.testFunction` + const and = "foobar"; + return true && and; + `.expectToMatchJsResult(); + }); + + test("variable (elseif)", () => { + util.testFunction` + const elseif: string | undefined = "foobar"; + if (false) { + } else if (elseif) { + return elseif; + } + `.expectToMatchJsResult(); + }); + + test("variable (end)", () => { + util.testFunction` + const end = "foobar"; + { + return end; + } + `.expectToMatchJsResult(); + }); + + test("variable (local)", () => { + util.testFunction` + const local = "foobar"; + return local; + `.expectToMatchJsResult(); + }); + + test("variable (not)", () => { + util.testFunction` + const not = "foobar"; + return (!false) && not; + `.expectToMatchJsResult(); + }); + + test("variable (or)", () => { + util.testFunction` + const or = "foobar"; + return false || or; + `.expectToMatchJsResult(); + }); + + test("variable (repeat)", () => { + util.testFunction` + const repeat = "foobar"; + do {} while (false); + return repeat; + `.expectToMatchJsResult(); + }); + + test("variable (then)", () => { + util.testFunction` + const then: string | undefined = "foobar"; + if (then) { + return then; + } + `.expectToMatchJsResult(); + }); + + test("variable (until)", () => { + util.testFunction` + const until = "foobar"; + do {} while (false); + return until; + `.expectToMatchJsResult(); + }); + + test("variable (goto)", () => { + util.testFunction` + const goto = "foobar"; + switch (goto) { + case goto: + return goto; + } + `.expectToMatchJsResult(); + }); + + test("variable (print)", () => { + const luaHeader = ` + local result = "" + print = function(s) + result = result .. s + end`; + + const tsHeader = ` + declare let result: string;`; + + const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; + + util.testFunction` + const print = "foobar"; + console.log(print); + return result; + ` + .setLuaHeader(luaHeader) + .setTsHeader(tsHeader) + .setOptions(compilerOptions) + .expectToEqual("foobar"); + }); + + test("variable (type)", () => { + util.testFunction` + function type(this: void, a: unknown) { + return (typeof a) + "|foobar"; + } + return type(7); + `.expectToMatchJsResult(); + }); + + test("variable (error)", () => { + const executionResult = util.testFunction` + const error = "foobar"; + throw error; + `.getLuaExecutionResult(); + + expect(executionResult).toEqual(new util.ExecutionError("foobar")); + }); + + test("variable (assert)", () => { + const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; + + const luaResult = util.testFunction` + const assert = false; + console.assert(assert, "foobar"); + ` + .setOptions(compilerOptions) + .getLuaExecutionResult(); + + expect(luaResult).toEqual(new util.ExecutionError("foobar")); + }); + + test("variable (debug)", () => { + const luaHeader = ` + local result = "" + print = function(s) + result = result .. s + end`; + + const tsHeader = ` + declare let result: string;`; + + const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; + + const luaResult = util.testFunction` + const debug = "foobar"; + console.trace(debug); + return result; + ` + .setTsHeader(tsHeader) + .setLuaHeader(luaHeader) + .setOptions(compilerOptions) + .getLuaExecutionResult(); + + expect(luaResult).toMatch(/^foobar\nstack traceback.+/); + }); + + test("variable (string)", () => { + util.testFunction` + const string = "foobar"; + return string[0]; + `.expectToMatchJsResult(); + }); + + test("variable (math)", () => { + util.testFunction` + const math = -17; + return Math.abs(math); + `.expectToMatchJsResult(); + }); + + test("variable (table)", () => { + util.testFunction` + const table = ["foobar"]; + return table.pop(); + `.expectToMatchJsResult(); + }); + + test("variable (coroutine)", () => { + util.testFunction` + const coroutine = "foobar"; + function *foo() { yield coroutine; } + return foo().next().value; + `.expectToMatchJsResult(); + }); + + test("variable (pairs)", () => { + util.testFunction` + const pairs = {foobar: "foobar"}; + let result = ""; + for (const key in pairs) { + result += key; + } + return result; + `.expectToMatchJsResult(); + }); + + test("variable (pcall)", () => { + util.testFunction` + const pcall = "foobar"; + try {} finally {} + return pcall; + `.expectToMatchJsResult(); + }); + + test("variable (rawget)", () => { + util.testFunction` + const rawget = {foobar: "foobar"}; + return rawget.hasOwnProperty("foobar"); + `.expectToMatchJsResult(); + }); + + test("variable (require)", () => { + const luaHeader = 'package.loaded.someModule = {foo = "bar"}'; + + const luaResult = util.testModule` + const require = "foobar"; + export { foo } from "someModule"; + export const result = require; + ` + .setLuaHeader(luaHeader) + .getLuaExecutionResult(); + + expect(luaResult.result).toBe("foobar"); + }); + + test("variable (tostring)", () => { + util.testFunction` + const tostring = 17; + return tostring.toString(); + `.expectToMatchJsResult(); + }); + + test("variable (unpack)", () => { + // Can't use expectToMatchJsResult because above is not valid TS/JS + const luaHeader = "unpack = table.unpack"; + + const luaResult = util.testFunction` + const unpack = ["foo", "bar"]; + const [foo, bar] = unpack; + return foo + bar; + ` + .setLuaHeader(luaHeader) + .getLuaExecutionResult(); + + 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"; + (globalThis as any).foo = "foo"; + return (globalThis as any).foo + _G; + `.expectToMatchJsResult(); + }); + + test("function parameter", () => { + util.testFunction` + function foo(type: unknown) { + return \`\${typeof type}|\${type}\`; + } + return foo("foobar"); + `.expectToMatchJsResult(); + }); + + test("destructured property function parameter", () => { + util.testFunction` + function foo({type}: any) { + return \`\${typeof type}|\${type}\`; + } + return foo({type: "foobar"}); + `.expectToMatchJsResult(); + }); + + test("destructured array element function parameter", () => { + util.testFunction` + function foo([type]: any) { + return \`\${typeof type}|\${type}\`; + } + return foo(["foobar"]); + `.expectToMatchJsResult(); + }); + + test("property", () => { + util.testFunction` + const type = "foobar"; + const foo = { type: type }; + return type + "|" + foo.type + "|" + typeof type; + `.expectToMatchJsResult(); + }); + + test("shorthand property", () => { + util.testFunction` + const type = "foobar"; + const foo = { type }; + return type + "|" + foo.type + "|" + typeof type; + `.expectToMatchJsResult(); + }); + + test("destructured property", () => { + util.testFunction` + const foo = { type: "foobar" }; + const { type: type } = foo; + return type + "|" + foo.type + "|" + typeof type; + `.expectToMatchJsResult(); + }); + + test("destructured shorthand property", () => { + util.testFunction` + const foo = { type: "foobar" }; + const { type } = foo; + return type + "|" + foo.type + "|" + typeof type; + `.expectToMatchJsResult(); + }); + + test("destructured array element", () => { + util.testFunction` + const foo = ["foobar"]; + const [type] = foo; + return type + "|" + typeof type; + `.expectToMatchJsResult(); + }); + + test.each(["type", "type as type"])("imported variable (%p)", importName => { + // Can't use expectToMatchJsResult because above is not valid TS/JS + const luaHeader = 'package.loaded.someModule = {type = "foobar"}'; + + const luaResult = util.testModule` + import {${importName}} from "someModule"; + export const result = typeof 7 + "|" + type; + ` + .setLuaHeader(luaHeader) + .getLuaExecutionResult(); + + expect(luaResult.result).toBe("number|foobar"); + }); + + test("separately exported variable (%p)", () => { + util.testModule` + const type = "foobar"; + export { type } + export { type as mytype } + export const result = typeof type + "|" + type; + `.expectToMatchJsResult(); + }); + + test.each(["type", "type as type"])("re-exported variable with lua keyword as name (%p)", importName => { + // Can't use expectToMatchJsResult because above is not valid TS/JS + + const luaHeader = 'package.loaded.someModule = {type = "foobar"}'; + + const luaResult = util.testModule` + export { ${importName} } from "someModule"; + ` + .setLuaHeader(luaHeader) + .getLuaExecutionResult(); + + expect(luaResult.type).toBe("foobar"); + }); + + test("class", () => { + util.testFunction` + class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + const t = new type(); + return t.method() + "|" + type.staticMethod(); + `.expectToMatchJsResult(); + }); + + test("subclass of class", () => { + util.testFunction` + class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + class Foo extends type {} + const foo = new Foo(); + return foo.method() + "|" + Foo.staticMethod(); + `.expectToMatchJsResult(); + }); + + test.each(["result", "type ~= nil"])("exported class (%p)", returnExport => { + util.testModule` + export class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + const t = new type(); + export const result = t.method() + "|" + type.staticMethod(); + ` + .setReturnExport(returnExport) + .expectToMatchJsResult(); + }); + + test.each(["result", "type ~= nil"])("subclass of exported class (%p)", returnExport => { + util.testModule` + export class type { + method() { return typeof 0; } + static staticMethod() { return typeof "foo"; } + } + class Foo extends type {} + const foo = new Foo(); + export const result = foo.method() + "|" + Foo.staticMethod(); + ` + .setReturnExport(returnExport) + .expectToMatchJsResult(); + }); + + test("namespace", () => { + const tsHeader = ` + namespace type { + export const foo = "foobar"; + }`; + + const code = ` + return typeof type.foo + "|" + type.foo`; + + util.testFunction(code).setTsHeader(tsHeader).expectToMatchJsResult(); + }); + + test("exported namespace (%p)", () => { + util.testModule` + export namespace type { + export const foo = "foobar"; + } + export const result = typeof type.foo + "|" + type.foo; + `.expectToMatchJsResult(); + }); + + test("merged namespace", () => { + const tsHeader = ` + class type { + method() { return typeof 0; } + static staticMethod() { return typeof true; } + } + + namespace type { + export const foo = "foo"; + } + + namespace type { + export const bar = "bar"; + }`; + + const code = ` + const t = new type(); + return \`\${t.method()}|\${type.staticMethod()}|\${typeof type.foo}|\${type.foo}|\${type.bar}\`;`; + + util.testFunction(code).setTsHeader(tsHeader).expectToMatchJsResult(); + }); + + test.each(["result", "type ~= nil"])("exported merged namespace (%p)", returnExport => { + util.testModule` + export class type { + method() { return typeof 0; } + static staticMethod() { return typeof true; } + } + + export namespace type { + export const foo = "foo"; + } + + export namespace type { + export const bar = "bar"; + } + + const t = new type(); + export const result = \`\${t.method()}|\${type.staticMethod()}|\${typeof type.foo}|\${type.foo}|\${type.bar}\`; + ` + .setReturnExport(returnExport) + .expectToMatchJsResult(); + }); +}); + +test("declaration-only variable with lua keyword as name is not renamed", () => { + util.testFunction("type(7)") + .setTsHeader("declare function type(this: void, a: unknown): string;") + .expectLuaToMatchSnapshot(); +}); + +test("exported variable with lua keyword as name is not renamed", () => { + util.testModule` + export const print = "foobar"; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/846 +test("lua built-in as class method", () => { + util.testModule` + class MyClass { + error() { return "Error!"; } + } + export const result = new MyClass().error(); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/833 +test("lua built-in as object method", () => { + util.testModule` + const obj = { error: () => "Error!" }; + export const result = obj.error(); + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/789 +test("lua built-in as in constructor assignment", () => { + util.testModule` + class A { + constructor(public error: string){} + } + + export const result = new A("42").error; + `.expectToMatchJsResult(); +}); + +test("customName rename function", () => { + const result = util.testModule` + /** @customName test2 **/ + function test(this: void): number { return 3; } + export const result: number = test(); + ` + .expectToEqual({ result: 3 }) + .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("function test2()"); + expect(mainFile.lua).not.toContain("test()"); +}); + +test("customName rename variable", () => { + const result = util.testModule` + /** @customName result2 **/ + export const result: number = 3; + ` + .expectToEqual({ result2: 3 }) + .getLuaResult(); + + 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("result2 ="); + expect(mainFile.lua).not.toContain("result ="); +}); + +test("customName rename classes", () => { + const testModule = util.testModule` + /** @customName Class2 **/ + class Class { + test: string; + + constructor(test: string) { + this.test = test; + } + } + + export const result = new Class("hello world"); + `; + + const executionResult = testModule.getLuaExecutionResult(); + expect(executionResult.result).toBeDefined(); + expect(executionResult.result).toMatchObject({ test: "hello world" }); + + 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("local Class2 ="); + expect(mainFile.lua).not.toContain("local Class ="); +}); + +test("customName rename namespace", () => { + const testModule = util.testModule` + /** @customName Test2 **/ + namespace Test { + /** @customName Func2 **/ + export function Func(): string { + return "hi"; + } + } + + export const result = Test.Func(); + `; + + testModule.expectToEqual({ result: "hi" }); + + 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("Test2.Func2"); + expect(mainFile.lua).not.toContain("Test ="); + expect(mainFile.lua).not.toContain("Func("); +}); + +test("customName rename declared function", () => { + const testModule = util.testModule` + /** @customName Test2 **/ + declare function Test(this: void): void; + + Test(); + `; + + 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).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/importexport.spec.ts b/test/unit/importexport.spec.ts deleted file mode 100644 index 7aca64caf..000000000 --- a/test/unit/importexport.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import * as util from "../src/util"; -import { TSTLErrors } from "../../src/TSTLErrors"; -import { TranspileError } from "../../src/TranspileError"; - -export class ImportExportTests -{ - @TestCase("export { default } from '...'") - @TestCase("export { x as default } from '...';") - @TestCase("export { default as x } from '...';") - @Test("Export default keyword disallowed") - public exportDefaultKeywordError(exportStatement: string): void { - const expectedTest = TSTLErrors.UnsupportedDefaultExport(undefined).message; - Expect(() => util.transpileString(exportStatement)).toThrowError(TranspileError, expectedTest); - } -} diff --git a/test/unit/json.spec.ts b/test/unit/json.spec.ts deleted file mode 100644 index 7ac15a776..000000000 --- a/test/unit/json.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { transpileString } from "../../src/Compiler"; -import { TranspileError } from "../../src/TranspileError"; -import * as util from "../src/util"; - -export class JsonTests { - @Test("JSON") - @TestCase("0") - @TestCase('""') - @TestCase("[]") - @TestCase('[1, "2", []]') - @TestCase('{ "a": "b" }') - @TestCase('{ "a": { "b": "c" } }') - public json(json: string): void { - const lua = transpileString(json, { resolveJsonModule: true, noHeader: true }, false, "file.json") - .replace(/^return ([\s\S]+);$/, "return JSONStringify($1);"); - - const result = util.executeLua(lua); - Expect(JSON.parse(result)).toEqual(JSON.parse(json)); - } - - @Test("Empty JSON") - public emptyJson(): void { - Expect(() => transpileString("", { resolveJsonModule: true, noHeader: true }, false, "file.json")) - .toThrowError(TranspileError, "Invalid JSON file content"); - } -} diff --git a/test/unit/jsx.spec.ts b/test/unit/jsx.spec.ts new file mode 100644 index 000000000..a72d0d7f9 --- /dev/null +++ b/test/unit/jsx.spec.ts @@ -0,0 +1,375 @@ +import * as util from "../util"; +import { TestBuilder } from "../util"; +import { JsxEmit } from "typescript"; +import { unsupportedJsxEmit } from "../../src/transpilation/diagnostics"; +import { unsupportedNodeKind } from "../../src/transformation/utils/diagnostics"; + +// language=TypeScript +const reactLib = ` + export class Component { + static isClass = true + } + + namespace React { + const isLua = typeof Component === "object" + + export function createElement( + type: any, + props?: any, + ...children: any[] + ) { + let typeStr: string + if (isLua) { + typeStr = + typeof type === "function" ? "<< function >>" : + typeof type === "object" ? \`<< class \${type.name} >>\` : + type + } else { + typeStr = typeof type === "function" ? (type.isClass + ? \`<< class \${type.name} >>\` + : "<< function >>") + : type + } + + return { + type: typeStr, + props, + children + }; + } + + export class Fragment extends Component { + } + } + export default React; +`; +// language=TypeScript +const jsxTypings = `declare namespace JSX { + interface IntrinsicElements { + a: any + b: any + foo: any + bar: any + div: any + "with-dash": any + } +} +`; + +function testJsx(...args: [string] | [TemplateStringsArray, ...any[]]): TestBuilder { + return util + .testFunction(...args) + .setOptions({ + jsx: JsxEmit.React, + }) + .setMainFileName("main.tsx") + .addExtraFile("react.ts", reactLib) + .addExtraFile("jsx.d.ts", jsxTypings) + .setTsHeader('import React, { Component } from "./react";'); +} + +describe("jsx", () => { + test("element", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("self closing element", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("element with dash name", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("custom element", () => { + testJsx` + function Foo() {} + return + `.expectToMatchJsResult(); + }); + test("fragment", () => { + testJsx` + return <> + `.expectToMatchJsResult(); + }); + test("fragment with children", () => { + testJsx` + return <> + `.expectToMatchJsResult(); + }); + test("esoteric component names", () => { + testJsx` + class _Foo extends Component {} + return <_Foo /> + `.expectToMatchJsResult(); + + testJsx` + class $ extends Component {} + return <$ /> + `.expectToMatchJsResult(); + + testJsx` + class é extends Component {} + return <é /> + `.expectToMatchJsResult(); + }); + test("nested elements", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("many nested elements", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("interpolated children", () => { + testJsx` + const x = 3 + return {x} + `.expectToMatchJsResult(); + }); + test("string prop", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("value prop", () => { + testJsx` + const x = 5 + return + `.expectToMatchJsResult(); + }); + test("quoted prop", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("shorthand prop", () => { + testJsx` + return + `.expectToMatchJsResult(); + }); + test("spaces in jsxText", () => { + testJsx` + return this + is somemultiline + text thing. + + + `.expectToMatchJsResult(); + }); + test("multiline string jsxText", () => { + testJsx` + return + foo bar + baz + + `.expectToMatchJsResult(); + }); + + test("access tag value", () => { + testJsx` + const a = { b(){} }; + return + `.expectToMatchJsResult(); + testJsx` + const a = { b: { c: { d(){} } } }; + return + `.expectToMatchJsResult(); + }); + + test("spread props", () => { + testJsx` + const x = {c: "d", e: "f"} + return + `.expectToMatchJsResult(); + testJsx` + const x = {c: "d", e: "no"} + return + `.expectToMatchJsResult(); + }); + + test("comment children", () => { + testJsx` + return + {/* comment */} + {/* another comment */} + + `.expectToMatchJsResult(); + testJsx` + return + + {/* comment */} + + + `.expectToMatchJsResult(); + }); + + test("multiline string prop value", () => { + testJsx` + return
+ `.expectToMatchJsResult(); + testJsx` + return
+ `.expectToMatchJsResult(); + }); + + test("prop strings with entities", () => { + testJsx` + return
+ `.expectToMatchJsResult(); + }); + test("jsxText with entities", () => { + testJsx` + return 9+10<21 + `.expectToMatchJsResult(); + }); + + test("Spread children", () => { + // doesn't actually "spread" (typescript's current behavior) + testJsx` + const children = [, ] + return {...children} + `.expectToMatchJsResult(); + }); + + test("complex", () => { + testJsx` + const x = 3 + const props = {one: "two", three: 4} + return + `.expectToMatchJsResult(); + }); + + // language=TypeScript + const customJsxLib = `export namespace MyLib { + export function myCreate( + type: any, + props: any, + ...children: any[] + ) { + return { type: typeof type, props, children, myThing: true }; + } + + export function MyFragment() { + } + } + `; + + test("custom JSX factory", () => { + testJsx` + return c + ` + .setTsHeader('import { MyLib } from "./myJsx";') + .setOptions({ jsxFactory: "MyLib.myCreate" }) + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + testJsx` + return c + ` + .setTsHeader('import { MyLib } from "./myJsx";const myCreate2 = MyLib.myCreate;') + .setOptions({ jsxFactory: "myCreate2" }) + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + }); + test("custom JSX factory with noImplicitSelf", () => { + testJsx` + return c + ` + .setTsHeader( + `function createElement(tag: string | Function, props: { [key: string]: string | boolean }, ...children: any[]) { + return { tag, children }; + }` + ) + .setOptions({ jsxFactory: "createElement", noImplicitSelf: true }) + .expectToMatchJsResult(); + }); + test("custom fragment factory", () => { + testJsx` + return <>c + ` + .setTsHeader('import { MyLib } from "./myJsx";') + .setOptions({ jsxFactory: "MyLib.myCreate", jsxFragmentFactory: "MyLib.MyFragment" }) + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + testJsx` + return <>c + ` + .setTsHeader('import { MyLib } from "./myJsx";function MyFragment2(){};') + .setOptions({ jsxFactory: "MyLib.myCreate", jsxFragmentFactory: "MyFragment2" }) + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + }); + test("custom JSX pragma", () => { + testJsx` + return c + ` + .setTsHeader('/** @jsx MyLib.myCreate */\nimport { MyLib } from "./myJsx";') + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + testJsx` + return c + ` + .setTsHeader('/** @jsx myCreate2 */import { MyLib } from "./myJsx";const myCreate2 = MyLib.myCreate;') + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + }); + test("custom fragment pragma", () => { + testJsx` + return <>c + ` + .setTsHeader( + '/** @jsx MyLib.myCreate */\n/** @jsxFrag MyLib.MyFragment */\nimport { MyLib } from "./myJsx";' + ) + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + testJsx` + return <>c + ` + .setTsHeader( + "/** @jsx MyLib.myCreate */\n/** @jsxFrag MyFragment2 */\n" + + 'import { MyLib } from "./myJsx";function MyFragment2(){};' + ) + .setOptions({ jsxFactory: "MyLib.myCreate", jsxFragmentFactory: "MyFragment" }) + .addExtraFile("myJsx.ts", customJsxLib) + .expectToMatchJsResult(); + }); + + test("forward declare components", () => { + testJsx` + const foo = + + function Foo(){} + + return foo + `.expectToMatchJsResult(); + }); + + test("invalid jsx config", () => { + testJsx(` + return + `) + .setOptions({ + jsx: JsxEmit.Preserve, + }) + .expectToHaveDiagnostics([unsupportedJsxEmit.code, unsupportedNodeKind.code]); + }); +}); diff --git a/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap new file mode 100644 index 000000000..e3c25d948 --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/iterable.spec.ts.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LuaIterable with LuaMultiReturn value type invalid LuaIterable without destructuring ("for (const s of testIterable()) {}"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function testIterable() + local strsArray = {{"a1", {a = "a"}}, {"b1", {a = "b"}}, {"c1", {a = "c"}}} + local i = 0 + return function() + local ____i_0 = i + i = ____i_0 + 1 + local strs = strsArray[____i_0 + 1] + if strs then + return table.unpack(strs) + end + end + end + for ____ in testIterable() do + end +end +return ____exports" +`; + +exports[`LuaIterable with LuaMultiReturn value type invalid LuaIterable without destructuring ("for (const s of testIterable()) {}"): diagnostics 1`] = `"main.ts(14,24): error TSTL: LuaIterable with a LuaMultiReturn return value type must be destructured."`; + +exports[`LuaIterable with LuaMultiReturn value type invalid LuaIterable without destructuring ("let s; for (s of testIterable()) {}"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function testIterable() + local strsArray = {{"a1", {a = "a"}}, {"b1", {a = "b"}}, {"c1", {a = "c"}}} + local i = 0 + return function() + local ____i_0 = i + i = ____i_0 + 1 + local strs = strsArray[____i_0 + 1] + if strs then + return table.unpack(strs) + end + end + end + local s + for ____ in testIterable() do + end +end +return ____exports" +`; + +exports[`LuaIterable with LuaMultiReturn value type invalid LuaIterable without destructuring ("let s; for (s of testIterable()) {}"): diagnostics 1`] = `"main.ts(14,25): error TSTL: LuaIterable with a LuaMultiReturn return value type must be destructured."`; diff --git a/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap new file mode 100644 index 000000000..a3f6de381 --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/multi.spec.ts.snap @@ -0,0 +1,233 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disallow LuaMultiReturn non-numeric access: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function multi(self, ...) + return ... + end + return select( + "forEach", + multi(nil) + ) +end +return ____exports" +`; + +exports[`disallow LuaMultiReturn non-numeric access: code 2`] = ` +"local ____exports = {} +function ____exports.__main(self) + local function multi(self, ...) + return ... + end + return ({multi(nil)}).forEach +end +return ____exports" +`; + +exports[`disallow LuaMultiReturn non-numeric access: diagnostics 1`] = `"main.ts(7,16): error TSTL: The LuaMultiReturn type can only be accessed via an element access expression of a numeric type."`; + +exports[`disallow LuaMultiReturn non-numeric access: diagnostics 2`] = `"main.ts(7,16): error TSTL: The LuaMultiReturn type can only be accessed via an element access expression of a numeric type."`; + +exports[`invalid $multi call ($multi()): code 1`] = `"____(_G)"`; + +exports[`invalid $multi call ($multi()): diagnostics 1`] = `"main.ts(2,9): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi call ($multi): code 1`] = `"local ____ = ____"`; + +exports[`invalid $multi call ($multi): diagnostics 1`] = `"main.ts(2,9): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi call (([a] = $multi(1)) => {}): code 1`] = ` +"local function ____(____, ____bindingPattern0) + if ____bindingPattern0 == nil then + ____bindingPattern0 = {____(_G, 1)} + end + local a = ____bindingPattern0[1] +end" +`; + +exports[`invalid $multi call (([a] = $multi(1)) => {}): diagnostics 1`] = `"main.ts(2,16): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi call (({ $multi });): code 1`] = `"local ____ = {["$multi"] = ____}"`; + +exports[`invalid $multi call (({ $multi });): diagnostics 1`] = `"main.ts(2,12): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi call (const [a = 0] = $multi()): code 1`] = ` +"a = ____(_G) +if a == nil then + a = 0 +end" +`; + +exports[`invalid $multi call (const [a = 0] = $multi()): diagnostics 1`] = `"main.ts(2,25): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi call (const {} = $multi();): code 1`] = `"local ____temp_0 = {{____(_G)}}"`; + +exports[`invalid $multi call (const {} = $multi();): diagnostics 1`] = `"main.ts(2,20): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi call (const a = $multi();): code 1`] = `"a = {____(_G)}"`; + +exports[`invalid $multi call (const a = $multi();): diagnostics 1`] = `"main.ts(2,19): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid $multi implicit cast: code 1`] = ` +"function badMulti(self) + return "foo", 42 +end" +`; + +exports[`invalid $multi implicit cast: diagnostics 1`] = `"main.ts(3,20): error TSTL: The $multi function cannot be cast to a non-LuaMultiReturn type."`; + +exports[`invalid direct $multi function use (const [a = 1] = $multi()): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a = ____(nil) +if a == nil then + a = 1 +end +____exports.a = a +return ____exports" +`; + +exports[`invalid direct $multi function use (const [a = 1] = $multi()): diagnostics 1`] = `"main.ts(7,25): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid direct $multi function use (const [a = 1] = $multi(2)): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a = ____(nil, 2) +if a == nil then + a = 1 +end +____exports.a = a +return ____exports" +`; + +exports[`invalid direct $multi function use (const [a = 1] = $multi(2)): diagnostics 1`] = `"main.ts(7,25): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid direct $multi function use (const [a] = $multi()): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a = ____(nil) +____exports.a = a +return ____exports" +`; + +exports[`invalid direct $multi function use (const [a] = $multi()): diagnostics 1`] = `"main.ts(7,21): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid direct $multi function use (const [a] = $multi(1)): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a = ____(nil, 1) +____exports.a = a +return ____exports" +`; + +exports[`invalid direct $multi function use (const [a] = $multi(1)): diagnostics 1`] = `"main.ts(7,21): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid direct $multi function use (const _ = null, [a] = $multi(1)): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local _ = nil +local a = ____(nil, 1) +____exports.a = a +return ____exports" +`; + +exports[`invalid direct $multi function use (const _ = null, [a] = $multi(1)): diagnostics 1`] = `"main.ts(7,31): error TSTL: The $multi function must be called in a return statement."`; + +exports[`invalid direct $multi function use (const ar = [1]; const [a] = $multi(...ar)): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local ar = {1} +local a = ____( + nil, + table.unpack(ar) +) +____exports.a = a +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`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a +do + local ____temp_0 = {____(nil, 1, 2)} + a = ____temp_0[1] + ____exports.a = a + while false do + local ____ = 1 + end +end +____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; for (const [a] = $multi(1, 2); false; 1) {}): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a +do + local a = ____(nil, 1, 2) + while false do + local ____ = 1 + end +end +____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; if ([a] = $multi(1)) { ++a; }): code 1`] = ` +"local ____exports = {} +local function multi(self, ...) + return ... +end +local a +local ____temp_0 = {____(nil, 1)} +a = ____temp_0[1] +____exports.a = a +if ____temp_0 then + a = a + 1 + ____exports.a = a +end +____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."`; diff --git a/test/unit/language-extensions/__snapshots__/operators.spec.ts.snap b/test/unit/language-extensions/__snapshots__/operators.spec.ts.snap new file mode 100644 index 000000000..a4554a8a1 --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/operators.spec.ts.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`does not crash on invalid operator use global function: code 1`] = `""`; + +exports[`does not crash on invalid operator use global function: diagnostics 1`] = `"main.ts(3,13): error TS2554: Expected 2 arguments, but got 1."`; + +exports[`does not crash on invalid operator use method: code 1`] = `"left = {}"`; + +exports[`does not crash on invalid operator use method: diagnostics 1`] = `"main.ts(5,18): error TS2554: Expected 1 arguments, but got 0."`; + +exports[`does not crash on invalid operator use unary operator: code 1`] = `"op(_G)"`; + +exports[`does not crash on invalid operator use unary operator: diagnostics 1`] = `"main.ts(2,31): error TS2304: Cannot find name 'LuaUnaryMinus'."`; + +exports[`operator mapping - invalid use (const foo = (op as any)(1, 2);): code 1`] = `"foo = op(_G, 1, 2)"`; + +exports[`operator mapping - invalid use (const foo = (op as any)(1, 2);): diagnostics 1`] = `"main.ts(3,22): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`operator mapping - invalid use (const foo = [op];): code 1`] = `"foo = {op}"`; + +exports[`operator mapping - invalid use (const foo = [op];): diagnostics 1`] = `"main.ts(3,22): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`operator mapping - invalid use (const foo = \`\${op}\`): code 1`] = `"foo = tostring(op)"`; + +exports[`operator mapping - invalid use (const foo = \`\${op}\`): diagnostics 1`] = `"main.ts(3,24): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`operator mapping - invalid use (const foo: unknown = op;): code 1`] = `"foo = op"`; + +exports[`operator mapping - invalid use (const foo: unknown = op;): diagnostics 1`] = `"main.ts(3,30): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`operator mapping - invalid use (declare function foo(op: LuaAddition): void; foo(op);): code 1`] = `"foo(_G, op)"`; + +exports[`operator mapping - invalid use (declare function foo(op: LuaAddition): void; foo(op);): diagnostics 1`] = `"main.ts(3,82): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseAnd): code 1`] = `"local ____ = left & right"`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseAnd): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseExclusiveOr): code 1`] = `"local ____ = left ~ right"`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseExclusiveOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseLeftShift): code 1`] = `"local ____ = left << right"`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseLeftShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseOr): code 1`] = `"local ____ = left | right"`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseRightShift): code 1`] = `"local ____ = left >> right"`; + +exports[`unsupported binary operator mapping (5.0 LuaBitwiseRightShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`unsupported binary operator mapping (5.0 LuaFloorDivision): code 1`] = `"local ____ = left // right"`; + +exports[`unsupported binary operator mapping (5.0 LuaFloorDivision): diagnostics 1`] = `"main.ts(6,9): error TSTL: Floor division operator is/are not supported for target Lua 5.0."`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseAnd): code 1`] = `"local ____ = left & right"`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseAnd): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseExclusiveOr): code 1`] = `"local ____ = left ~ right"`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseExclusiveOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseLeftShift): code 1`] = `"local ____ = left << right"`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseLeftShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseOr): code 1`] = `"local ____ = left | right"`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseRightShift): code 1`] = `"local ____ = left >> right"`; + +exports[`unsupported binary operator mapping (5.1 LuaBitwiseRightShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (5.1 LuaFloorDivision): code 1`] = `"local ____ = left // right"`; + +exports[`unsupported binary operator mapping (5.1 LuaFloorDivision): diagnostics 1`] = `"main.ts(6,9): error TSTL: Floor division operator is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseAnd): code 1`] = `"local ____ = left & right"`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseAnd): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.2."`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseExclusiveOr): code 1`] = `"local ____ = left ~ right"`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseExclusiveOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.2."`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseLeftShift): code 1`] = `"local ____ = left << right"`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseLeftShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.2."`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseOr): code 1`] = `"local ____ = left | right"`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.2."`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseRightShift): code 1`] = `"local ____ = left >> right"`; + +exports[`unsupported binary operator mapping (5.2 LuaBitwiseRightShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.2."`; + +exports[`unsupported binary operator mapping (5.2 LuaFloorDivision): code 1`] = `"local ____ = left // right"`; + +exports[`unsupported binary operator mapping (5.2 LuaFloorDivision): diagnostics 1`] = `"main.ts(6,9): error TSTL: Floor division operator is/are not supported for target Lua 5.2."`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseAnd): code 1`] = `"local ____ = left & right"`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseAnd): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target LuaJIT."`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseExclusiveOr): code 1`] = `"local ____ = left ~ right"`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseExclusiveOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target LuaJIT."`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseLeftShift): code 1`] = `"local ____ = left << right"`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseLeftShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target LuaJIT."`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseOr): code 1`] = `"local ____ = left | right"`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target LuaJIT."`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseRightShift): code 1`] = `"local ____ = left >> right"`; + +exports[`unsupported binary operator mapping (JIT LuaBitwiseRightShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target LuaJIT."`; + +exports[`unsupported binary operator mapping (JIT LuaFloorDivision): code 1`] = `"local ____ = left // right"`; + +exports[`unsupported binary operator mapping (JIT LuaFloorDivision): diagnostics 1`] = `"main.ts(6,9): error TSTL: Floor division operator is/are not supported for target LuaJIT."`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseAnd): code 1`] = `"local ____ = left & right"`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseAnd): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseExclusiveOr): code 1`] = `"local ____ = left ~ right"`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseExclusiveOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseLeftShift): code 1`] = `"local ____ = left << right"`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseLeftShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseOr): code 1`] = `"local ____ = left | right"`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseOr): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseRightShift): code 1`] = `"local ____ = left >> right"`; + +exports[`unsupported binary operator mapping (universal LuaBitwiseRightShift): diagnostics 1`] = `"main.ts(6,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported binary operator mapping (universal LuaFloorDivision): code 1`] = `"local ____ = left // right"`; + +exports[`unsupported binary operator mapping (universal LuaFloorDivision): diagnostics 1`] = `"main.ts(6,9): error TSTL: Floor division operator is/are not supported for target Lua 5.1."`; + +exports[`unsupported unary operator mapping (5.0 LuaBitwiseNot): code 1`] = `"local ____ = ~operand"`; + +exports[`unsupported unary operator mapping (5.0 LuaBitwiseNot): diagnostics 1`] = `"main.ts(5,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.0."`; + +exports[`unsupported unary operator mapping (5.1 LuaBitwiseNot): code 1`] = `"local ____ = ~operand"`; + +exports[`unsupported unary operator mapping (5.1 LuaBitwiseNot): diagnostics 1`] = `"main.ts(5,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; + +exports[`unsupported unary operator mapping (5.2 LuaBitwiseNot): code 1`] = `"local ____ = ~operand"`; + +exports[`unsupported unary operator mapping (5.2 LuaBitwiseNot): diagnostics 1`] = `"main.ts(5,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.2."`; + +exports[`unsupported unary operator mapping (JIT LuaBitwiseNot): code 1`] = `"local ____ = ~operand"`; + +exports[`unsupported unary operator mapping (JIT LuaBitwiseNot): diagnostics 1`] = `"main.ts(5,9): error TSTL: Native bitwise operations is/are not supported for target LuaJIT."`; + +exports[`unsupported unary operator mapping (universal LuaBitwiseNot): code 1`] = `"local ____ = ~operand"`; + +exports[`unsupported unary operator mapping (universal LuaBitwiseNot): diagnostics 1`] = `"main.ts(5,9): error TSTL: Native bitwise operations is/are not supported for target Lua 5.1."`; diff --git a/test/unit/language-extensions/__snapshots__/pairsIterable.spec.ts.snap b/test/unit/language-extensions/__snapshots__/pairsIterable.spec.ts.snap new file mode 100644 index 000000000..f38856514 --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/pairsIterable.spec.ts.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`invalid LuaPairsIterable without destructuring ("for (const s of testIterable) {}"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local testIterable = {a1 = "a2", b1 = "b2", c1 = "c2"} + for ____ in pairs(testIterable) do + end +end +return ____exports" +`; + +exports[`invalid LuaPairsIterable without destructuring ("for (const s of testIterable) {}"): diagnostics 1`] = `"main.ts(5,20): error TSTL: LuaPairsIterable type must be destructured in a for...of statement."`; + +exports[`invalid LuaPairsIterable without destructuring ("let s; for (s of testIterable) {}"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local testIterable = {a1 = "a2", b1 = "b2", c1 = "c2"} + local s + for ____ in pairs(testIterable) do + end +end +return ____exports" +`; + +exports[`invalid LuaPairsIterable without destructuring ("let s; for (s of testIterable) {}"): diagnostics 1`] = `"main.ts(5,21): error TSTL: LuaPairsIterable type must be destructured in a for...of statement."`; diff --git a/test/unit/language-extensions/__snapshots__/range.spec.ts.snap b/test/unit/language-extensions/__snapshots__/range.spec.ts.snap new file mode 100644 index 000000000..73a56890b --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/range.spec.ts.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`$range invalid control variable: code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local i + for ____ = 1, 5 do + end +end +return ____exports" +`; + +exports[`$range invalid control variable: diagnostics 1`] = `"main.ts(3,14): error TSTL: For loop using $range must declare a single control variable."`; + +exports[`$range invalid use ("const range = $range;"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local range = ____ +end +return ____exports" +`; + +exports[`$range invalid use ("const range = $range;"): diagnostics 1`] = `"main.ts(2,23): error TSTL: $range can only be used in a for...of loop."`; + +exports[`$range invalid use ("const x = $range(1, 10);"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = ____(nil, 1, 10) +end +return ____exports" +`; + +exports[`$range invalid use ("const x = $range(1, 10);"): diagnostics 1`] = `"main.ts(2,19): error TSTL: $range can only be used in a for...of loop."`; + +exports[`$range invalid use ("const y = [...$range(1, 10)];"): code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Spread = ____lualib.__TS__Spread +local ____exports = {} +function ____exports.__main(self) + local y = {__TS__Spread(____(nil, 1, 10))} +end +return ____exports" +`; + +exports[`$range invalid use ("const y = [...$range(1, 10)];"): diagnostics 1`] = `"main.ts(2,23): error TSTL: $range can only be used in a for...of loop."`; + +exports[`$range invalid use ("for (const i in $range(1, 10, 2)) {}"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + for i in pairs(____(nil, 1, 10, 2)) do + end +end +return ____exports" +`; + +exports[`$range invalid use ("for (const i in $range(1, 10, 2)) {}"): diagnostics 1`] = `"main.ts(2,25): error TSTL: $range can only be used in a for...of loop."`; + +exports[`$range invalid use ("for (const i of $range(1, 10, 2) as number[]) {}"): code 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + for ____, i in ipairs(____(nil, 1, 10, 2)) do + end +end +return ____exports" +`; + +exports[`$range invalid use ("for (const i of $range(1, 10, 2) as number[]) {}"): diagnostics 1`] = `"main.ts(2,25): error TSTL: $range can only be used in a for...of loop."`; + +exports[`$range invalid use ("for (const i of ($range(1, 10, 2))) {}"): code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Iterator = ____lualib.__TS__Iterator +local ____exports = {} +function ____exports.__main(self) + for ____, i in __TS__Iterator(____(nil, 1, 10, 2)) do + end +end +return ____exports" +`; + +exports[`$range invalid use ("for (const i of ($range(1, 10, 2))) {}"): diagnostics 1`] = `"main.ts(2,26): error TSTL: $range can only be used in a for...of loop."`; diff --git a/test/unit/language-extensions/__snapshots__/table.spec.ts.snap b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap new file mode 100644 index 000000000..24abd6e71 --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/table.spec.ts.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("null"): code 1`] = ` +"local ____exports = {} +____exports.__result = {} +return ____exports" +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("null"): diagnostics 1`] = `"main.ts(1,38): error TS2344: Type 'null' does not satisfy the constraint 'AnyNotNil'."`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("number | undefined"): code 1`] = ` +"local ____exports = {} +____exports.__result = {} +return ____exports" +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("number | undefined"): diagnostics 1`] = ` +"main.ts(1,38): error TS2344: Type 'number | undefined' does not satisfy the constraint 'AnyNotNil'. + Type 'undefined' is not assignable to type 'AnyNotNil'." +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("string | null"): code 1`] = ` +"local ____exports = {} +____exports.__result = {} +return ____exports" +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("string | null"): diagnostics 1`] = ` +"main.ts(1,38): error TS2344: Type 'string | null' does not satisfy the constraint 'AnyNotNil'. + Type 'null' is not assignable to type 'AnyNotNil'." +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("undefined"): code 1`] = ` +"local ____exports = {} +____exports.__result = {} +return ____exports" +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("undefined"): diagnostics 1`] = `"main.ts(1,38): error TS2344: Type 'undefined' does not satisfy the constraint 'AnyNotNil'."`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("unknown"): code 1`] = ` +"local ____exports = {} +____exports.__result = {} +return ____exports" +`; + +exports[`LuaTable extension interface LuaTable in strict mode does not accept key type that could be nil ("unknown"): diagnostics 1`] = `"main.ts(1,38): error TS2344: Type 'unknown' does not satisfy the constraint 'AnyNotNil'."`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo = (getTable as any)(1, 2);"): code 1`] = `"foo = getTable(_G, 1, 2)"`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo = (getTable as any)(1, 2);"): diagnostics 1`] = `"main.ts(3,26): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo = [getTable];"): code 1`] = `"foo = {getTable}"`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo = [getTable];"): diagnostics 1`] = `"main.ts(3,26): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo = \`\${getTable}\`;"): code 1`] = `"foo = tostring(getTable)"`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo = \`\${getTable}\`;"): diagnostics 1`] = `"main.ts(3,28): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo: unknown = getTable;"): code 1`] = `"foo = getTable"`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("const foo: unknown = getTable;"): diagnostics 1`] = `"main.ts(3,34): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("declare function foo(getTable: LuaTableGet<{}, string, number>): void; foo(getTable);"): code 1`] = `"foo(_G, getTable)"`; + +exports[`LuaTableGet & LuaTableSet extensions invalid use ("declare function foo(getTable: LuaTableGet<{}, string, number>): void; foo(getTable);"): diagnostics 1`] = `"main.ts(3,88): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use ("const foo = (tableHas as any)(1, 2);"): code 1`] = `"foo = tableHas(_G, 1, 2)"`; + +exports[`LuaTableHas extension invalid use ("const foo = (tableHas as any)(1, 2);"): diagnostics 1`] = `"main.ts(3,26): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use ("const foo = [tableHas];"): code 1`] = `"foo = {tableHas}"`; + +exports[`LuaTableHas extension invalid use ("const foo = [tableHas];"): diagnostics 1`] = `"main.ts(3,26): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use ("const foo = \`\${tableHas}\`;"): code 1`] = `"foo = tostring(tableHas)"`; + +exports[`LuaTableHas extension invalid use ("const foo = \`\${tableHas}\`;"): diagnostics 1`] = `"main.ts(3,28): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use ("const foo: unknown = tableHas;"): code 1`] = `"foo = tableHas"`; + +exports[`LuaTableHas extension invalid use ("const foo: unknown = tableHas;"): diagnostics 1`] = `"main.ts(3,34): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use ("declare function foo(tableHas: LuaTableHas<{}, string>): void; foo(tableHas);"): code 1`] = `"foo(_G, tableHas)"`; + +exports[`LuaTableHas extension invalid use ("declare function foo(tableHas: LuaTableHas<{}, string>): void; foo(tableHas);"): diagnostics 1`] = `"main.ts(3,80): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use method assignment ("LuaMap"): code 1`] = ` +"____table = {} +has = ____table.has" +`; + +exports[`LuaTableHas extension invalid use method assignment ("LuaMap"): diagnostics 1`] = `"main.ts(3,29): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use method assignment ("LuaSet"): code 1`] = ` +"____table = {} +has = ____table.has" +`; + +exports[`LuaTableHas extension invalid use method assignment ("LuaSet"): diagnostics 1`] = `"main.ts(3,29): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use method assignment ("LuaTable"): code 1`] = ` +"____table = {} +has = ____table.has" +`; + +exports[`LuaTableHas extension invalid use method assignment ("LuaTable"): diagnostics 1`] = `"main.ts(3,29): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use method expression ("LuaMap"): code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__ArrayMap = ____lualib.__TS__ArrayMap +____table = {} +__TS__ArrayMap({"a", "b", "c"}, ____table.has)" +`; + +exports[`LuaTableHas extension invalid use method expression ("LuaMap"): diagnostics 1`] = `"main.ts(3,37): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use method expression ("LuaSet"): code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__ArrayMap = ____lualib.__TS__ArrayMap +____table = {} +__TS__ArrayMap({"a", "b", "c"}, ____table.has)" +`; + +exports[`LuaTableHas extension invalid use method expression ("LuaSet"): diagnostics 1`] = `"main.ts(3,37): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`LuaTableHas extension invalid use method expression ("LuaTable"): code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__ArrayMap = ____lualib.__TS__ArrayMap +____table = {} +__TS__ArrayMap({"a", "b", "c"}, ____table.has)" +`; + +exports[`LuaTableHas extension invalid use method expression ("LuaTable"): diagnostics 1`] = `"main.ts(3,37): error TSTL: This function must be called directly and cannot be referred to."`; + +exports[`does not crash on invalid extension use global function: code 1`] = `""`; + +exports[`does not crash on invalid extension use global function: diagnostics 1`] = `"main.ts(3,9): error TS2554: Expected 2 arguments, but got 1."`; + +exports[`does not crash on invalid extension use method: code 1`] = `"left = {}"`; + +exports[`does not crash on invalid extension use method: diagnostics 1`] = `"main.ts(5,14): error TS2554: Expected 2 arguments, but got 0."`; diff --git a/test/unit/language-extensions/__snapshots__/vararg.spec.ts.snap b/test/unit/language-extensions/__snapshots__/vararg.spec.ts.snap new file mode 100644 index 000000000..e2cf42298 --- /dev/null +++ b/test/unit/language-extensions/__snapshots__/vararg.spec.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`$vararg invalid use ("const l = $vararg.length"): code 1`] = `"l = #____"`; + +exports[`$vararg invalid use ("const l = $vararg.length"): diagnostics 1`] = `"main.ts(2,19): error TSTL: $vararg can only be used in a spread element ('...$vararg') in global scope."`; + +exports[`$vararg invalid use ("const x = $vararg;"): code 1`] = `"x = ____"`; + +exports[`$vararg invalid use ("const x = $vararg;"): diagnostics 1`] = `"main.ts(2,19): error TSTL: $vararg can only be used in a spread element ('...$vararg') in global scope."`; + +exports[`$vararg invalid use ("for (const x of $vararg) {}"): code 1`] = ` +"for ____, x in ipairs(____) do +end" +`; + +exports[`$vararg invalid use ("for (const x of $vararg) {}"): diagnostics 1`] = `"main.ts(2,25): error TSTL: $vararg can only be used in a spread element ('...$vararg') in global scope."`; + +exports[`$vararg invalid use ("function f(s: string[]) {} f($vararg);"): code 1`] = ` +"function f(self, s) +end +f(_G, ____)" +`; + +exports[`$vararg invalid use ("function f(s: string[]) {} f($vararg);"): diagnostics 1`] = `"main.ts(2,38): error TSTL: $vararg can only be used in a spread element ('...$vararg') in global scope."`; + +exports[`$vararg invalid use ("function foo(...args: string[]) {} function bar() { foo(...$vararg); }"): code 1`] = ` +"local ____lualib = require("lualib_bundle") +local __TS__Spread = ____lualib.__TS__Spread +function foo(self, ...) +end +function bar(self) + foo( + _G, + __TS__Spread(____) + ) +end" +`; + +exports[`$vararg invalid use ("function foo(...args: string[]) {} function bar() { foo(...$vararg); }"): diagnostics 1`] = `"main.ts(2,68): error TSTL: $vararg can only be used in a spread element ('...$vararg') in global scope."`; diff --git a/test/unit/language-extensions/iterable.spec.ts b/test/unit/language-extensions/iterable.spec.ts new file mode 100644 index 000000000..30e602f62 --- /dev/null +++ b/test/unit/language-extensions/iterable.spec.ts @@ -0,0 +1,563 @@ +import * as util from "../../util"; +import { invalidMultiIterableWithoutDestructuring } from "../../../src/transformation/utils/diagnostics"; + +describe("simple LuaIterable", () => { + const testIterable = ` + function testIterable(this: void): LuaIterable { + const strs = ["a", "b", "c"]; + let i = 0; + return (() => strs[i++]) as any; + } + `; + const testResults = ["a", "b", "c"]; + + test("const control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + for (const s of testIterable()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("let control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + for (let s of testIterable()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("external control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + let s: string; + for (s of testIterable()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("function forward", () => { + util.testFunction` + ${testIterable} + function forward() { return testIterable(); } + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("function indirect forward", () => { + util.testFunction` + ${testIterable} + function forward() { const iter = testIterable(); return iter; } + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("arrow function forward", () => { + util.testFunction` + ${testIterable} + const forward = () => testIterable(); + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("manual use", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + const iter = testIterable(); + while (true) { + const val = iter(); + if (!val) { + break; + } + results.push(val); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); +}); + +describe("LuaIterable using state", () => { + const testIterable = ` + function iterator(this: void, strs: string[], lastStr: string) { + return strs[strs.indexOf(lastStr) + 1]; + } + const testIterable = (() => $multi(iterator, ["a", "b", "c"], "")) as (() => LuaIterable); + `; + const testResults = ["a", "b", "c"]; + + test("const control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + for (const s of testIterable()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("let control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + for (let s of testIterable()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("external control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + let s: string; + for (s of testIterable()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("function forward", () => { + util.testFunction` + ${testIterable} + function forward() { return testIterable(); } + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("function indirect forward", () => { + util.testFunction` + ${testIterable} + function forward() { const iter = testIterable(); return iter; } + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("arrow function forward", () => { + util.testFunction` + ${testIterable} + const forward = () => testIterable(); + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("manual use", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + let [iter, state, val] = testIterable(); + while (true) { + val = iter(state, val); + if (!val) { + break; + } + results.push(val); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); +}); + +describe("LuaIterable with array value type", () => { + const testIterable = ` + function testIterable(this: void): LuaIterable { + const strsArray = [["a1", "a2"], ["b1", "b2"], ["c1", "c2"]]; + let i = 0; + return (() => strsArray[i++]) as any; + } + `; + const testResults = [ + ["a1", "a2"], + ["b1", "b2"], + ["c1", "c2"], + ]; + + test("basic destructuring", () => { + util.testFunction` + ${testIterable} + const results = []; + for (const [x, y] of testIterable()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure with external control variable", () => { + util.testFunction` + ${testIterable} + const results = [] + let x: string, y: string; + for ([x, y] of testIterable()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure with function forward", () => { + util.testFunction` + ${testIterable} + function forward() { return testIterable(); } + const results: Array = []; + for (const [x, y] of forward()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure with function indirect forward", () => { + util.testFunction` + ${testIterable} + function forward() { const iter = testIterable(); return iter; } + const results: Array = []; + for (const [x, y] of forward()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure arrow function forward", () => { + util.testFunction` + ${testIterable} + const forward = () => testIterable(); + const results: Array = []; + for (const [x, y] of forward()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("manual use", () => { + util.testFunction` + ${testIterable} + const results: Array = []; + const iter = testIterable(); + while (true) { + const val = iter(); + if (!val) { + break; + } + results.push(val); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); +}); + +describe("LuaIterable with LuaMultiReturn value type", () => { + const testIterable = ` + function testIterable(this: void): LuaIterable> { + const strsArray = [["a1", {a: "a"}], ["b1", {a: "b"}], ["c1", {a: "c"}]]; + let i = 0; + return (() => { + const strs = strsArray[i++]; + if (strs) { + return $multi(...strs); + } + }) as any; + } + `; + const testResults = [ + ["a1", { a: "a" }], + ["b1", { a: "b" }], + ["c1", { a: "c" }], + ]; + + test("basic destructuring", () => { + util.testFunction` + ${testIterable} + const results = []; + for (const [x, y] of testIterable()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure with external control variable", () => { + util.testFunction` + ${testIterable} + const results = []; + let x: string, y: any; + for ([x, y] of testIterable()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure with function forward", () => { + util.testFunction` + ${testIterable} + function forward() { return testIterable(); } + const results = []; + for (const [x, y] of forward()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure with function indirect forward", () => { + util.testFunction` + ${testIterable} + function forward() { const iter = testIterable(); return iter; } + const results = []; + for (const [x, y] of forward()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure arrow function forward", () => { + util.testFunction` + ${testIterable} + const forward = () => testIterable(); + const results = []; + for (const [x, y] of forward()) { + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("destructure manual use", () => { + util.testFunction` + ${testIterable} + const results = []; + const iter = testIterable(); + while (true) { + const [x, y] = iter(); + if (!x) { + break; + } + results.push([x, y]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("nested destructuring", () => { + util.testFunction` + ${testIterable} + const results = []; + for (const [x, {a}] of testIterable()) { + results.push([x, a]); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual([ + ["a1", "a"], + ["b1", "b"], + ["c1", "c"], + ]); + }); + + test.each(["for (const s of testIterable()) {}", "let s; for (s of testIterable()) {}"])( + "invalid LuaIterable without destructuring (%p)", + statement => { + util.testFunction` + ${testIterable} + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidMultiIterableWithoutDestructuring.code]); + } + ); +}); + +describe("LuaIterable property", () => { + const testIterable = ` + class IterableTester { + public strs = ["a", "b", "c"]; + + public get values(): LuaIterable { + let i = 0; + return (() => this.strs[i++]) as any; + } + } + const tester = new IterableTester(); + `; + const testResults = ["a", "b", "c"]; + + test("basic usage", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + for (const s of tester.values) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("external control variable", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + let s: string; + for (s of tester.values) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("function forward", () => { + util.testFunction` + ${testIterable} + function forward() { return tester.values; } + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("function indirect forward", () => { + util.testFunction` + ${testIterable} + function forward() { const iter = tester.values; return iter; } + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("arrow function forward", () => { + util.testFunction` + ${testIterable} + const forward = () => tester.values; + const results: string[] = []; + for (const s of forward()) { + results.push(s); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); + + test("manual use", () => { + util.testFunction` + ${testIterable} + const results: string[] = []; + const iter = tester.values; + while (true) { + const val = iter(); + if (!val) { + break; + } + results.push(val); + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); + }); +}); diff --git a/test/unit/language-extensions/multi.spec.ts b/test/unit/language-extensions/multi.spec.ts new file mode 100644 index 000000000..72b330887 --- /dev/null +++ b/test/unit/language-extensions/multi.spec.ts @@ -0,0 +1,403 @@ +import * as util from "../../util"; +import { + invalidMultiFunctionUse, + invalidMultiReturnAccess, + invalidMultiFunctionReturnType, +} from "../../../src/transformation/utils/diagnostics"; + +test("multi example use case", () => { + util.testModule` + function multiReturn(): LuaMultiReturn<[string, number]> { + return $multi("foo", 5); + } + + const [a, b] = multiReturn(); + export { a, b }; + ` + .withLanguageExtensions() + .expectToEqual({ a: "foo", b: 5 }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/995 +test("Destructuring assignment of LuaMultiReturn", () => { + util.testModule` + function multiReturn(): LuaMultiReturn<[number, number, number]> { + return $multi(1, 2, 3); + } + + const [a, ...b] = multiReturn(); + export {a, b}; + ` + .withLanguageExtensions() + .expectToEqual({ a: 1, b: [2, 3] }); +}); + +test("Destructuring assignment of LuaMultiReturn returning nil", () => { + util.testModule` + function multiReturn(): LuaMultiReturn<[number, number, number]> { + return; + } + + const [a, ...b] = multiReturn(); + export {a, b}; + ` + .withLanguageExtensions() + .expectToEqual({ a: undefined, b: [] }); +}); + +test.each<[string, any]>([ + ["$multi()", undefined], + ["$multi(true)", true], + ["$multi(1, 2)", 1], +])("$multi call on return statement (%s)", (expression, result) => { + util.testFunction` + return ${expression}; + ` + .withLanguageExtensions() + .expectToEqual(result); +}); + +const multiFunction = ` +function multi(...args) { + return $multi(...args); +} +`; + +const createCasesThatCall = (name: string): Array<[string, any]> => [ + [`let a; [a] = ${name}()`, undefined], + [`const [a] = ${name}()`, undefined], + [`const [a] = ${name}(1)`, 1], + [`const [a = 1] = ${name}()`, 1], + [`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], +]; + +test.each<[string, any]>(createCasesThatCall("$multi"))("invalid direct $multi function use (%s)", statement => { + util.testModule` + ${multiFunction} + ${statement} + export { a }; + ` + .withLanguageExtensions() + .setReturnExport("a") + .expectDiagnosticsToMatchSnapshot([invalidMultiFunctionUse.code]); +}); + +test.each<[string, any]>(createCasesThatCall("multi"))( + "valid indirect $multi function use (%s)", + (statement, result) => { + util.testModule` + ${multiFunction} + ${statement} + export { a }; + ` + .withLanguageExtensions() + .setReturnExport("a") + .expectToEqual(result); + } +); + +test.each<[string, number[]]>([ + ["$multi", [invalidMultiFunctionUse.code]], + ["$multi()", [invalidMultiFunctionUse.code]], + ["({ $multi });", [invalidMultiFunctionUse.code]], + ["const a = $multi();", [invalidMultiFunctionUse.code]], + ["const {} = $multi();", [invalidMultiFunctionUse.code]], + ["([a] = $multi(1)) => {}", [invalidMultiFunctionUse.code]], + ["const [a = 0] = $multi()", [invalidMultiFunctionUse.code]], +])("invalid $multi call (%s)", (statement, diagnostics) => { + util.testModule` + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot(diagnostics); +}); + +test("function to spread multi type result from multi type function", () => { + util.testFunction` + ${multiFunction} + function m() { + return $multi(...multi(true)); + } + return m(); + ` + .withLanguageExtensions() + .expectToEqual(true); +}); + +test("$multi call with destructuring assignment side effects", () => { + util.testModule` + ${multiFunction} + let a; + export { a }; + [a] = multi(1); + ` + .withLanguageExtensions() + .setReturnExport("a") + .expectToEqual(1); +}); + +test("allow $multi call in ArrowFunction body", () => { + util.testFunction` + const call = () => $multi(1); + const [result] = call(); + return result; + ` + .withLanguageExtensions() + .expectToEqual(1); +}); + +test("forward $multi call", () => { + util.testFunction` + function foo() { return $multi(1, 2); } + function call() { return foo(); } + const [resultA, resultB] = call(); + return [resultA, resultB]; + ` + .withLanguageExtensions() + .expectToEqual([1, 2]); +}); + +test("forward $multi call indirect", () => { + util.testFunction` + function foo() { return $multi(1, 2); } + function call() { const m = foo(); return m; } + const [resultA, resultB] = call(); + return [resultA, resultB]; + ` + .withLanguageExtensions() + .expectToEqual([1, 2]); +}); + +test("forward $multi call in ArrowFunction body", () => { + util.testFunction` + const foo = () => $multi(1, 2); + const call = () => foo(); + const [resultA, resultB] = call(); + return [resultA, resultB]; + ` + .withLanguageExtensions() + .expectToEqual([1, 2]); +}); + +test("$multi call in function typed as any", () => { + util.testFunction` + function foo(): any { return $multi(1, 2); } + return foo() + ` + .withLanguageExtensions() + .expectToHaveNoDiagnostics() + .expectToEqual(1); +}); + +test("$multi call cast to any", () => { + util.testFunction` + function foo() { return $multi(1, 2) as any; } + return foo() + ` + .withLanguageExtensions() + .expectToHaveNoDiagnostics() + .expectToEqual(1); +}); + +test("$multi call cast to MultiReturn type", () => { + util.testFunction` + function foo() { return $multi(1, 2) as unknown as LuaMultiReturn; } + return foo() + ` + .withLanguageExtensions() + .expectToHaveNoDiagnostics() + .expectToEqual(1); +}); + +test.each(["0", "i"])("allow LuaMultiReturn numeric access (%s)", expression => { + util.testFunction` + ${multiFunction} + const i = 0; + return multi(1)[${expression}]; + ` + .withLanguageExtensions() + .expectToEqual(1); +}); + +test.each(["multi()['forEach']", "multi().forEach"])("disallow LuaMultiReturn non-numeric access", expression => { + util.testFunction` + ${multiFunction} + return ${expression}; + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidMultiReturnAccess.code]); +}); + +test("invalid $multi implicit cast", () => { + util.testModule` + function badMulti(): [string, number] { + return $multi("foo", 42); + } + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidMultiFunctionReturnType.code]); +}); + +test.each([ + { flag: true, result: 4 }, + { flag: false, result: 7 }, +])("overloaded $multi/non-$multi return", ({ flag, result }) => { + util.testFunction` + function multiOverload(flag: true): LuaMultiReturn<[string, number]>; + function multiOverload(flag: false): [string, number]; + function multiOverload(flag: boolean) { + if (flag) { + return $multi("foo", 4); + } else { + return ["bar", 7]; + } + } + const [x, y] = multiOverload(${flag}); + return y; + ` + .withLanguageExtensions() + .expectToEqual(result); +}); + +test("return $multi from try", () => { + util.testFunction` + function multiTest() { + try { + return $multi(1, 2); + } catch { + } + } + const [_, a] = multiTest(); + return a; + ` + .withLanguageExtensions() + .expectToEqual(2); +}); + +test("return $multi from catch", () => { + util.testFunction` + function multiTest() { + try { + throw "error"; + } catch { + return $multi(1, 2); + } + } + const [_, a] = multiTest(); + return a; + ` + .withLanguageExtensions() + .expectToEqual(2); +}); + +test("return LuaMultiReturn from try", () => { + util.testFunction` + ${multiFunction} + function multiTest() { + try { + return multi(1, 2); + } catch { + } + } + const [_, a] = multiTest(); + return a; + ` + .withLanguageExtensions() + .expectToEqual(2); +}); + +test("return LuaMultiReturn from catch", () => { + util.testFunction` + ${multiFunction} + function multiTest() { + try { + throw "error"; + } catch { + return multi(1, 2); + } + } + const [_, a] = multiTest(); + return a; + ` + .withLanguageExtensions() + .expectToEqual(2); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1404 +test("LuaMultiReturn applies after casting a function (#1404)", () => { + util.testFunction` + let swap: any = (a: number, b: number) => $multi(b, a); + let [a, b] = (swap as (...args: any) => LuaMultiReturn<[number, number]>)(4, 3); + return [a, b]; + ` + .withLanguageExtensions() + .expectToEqual([3, 4]); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1411 +describe("LuaMultiReturn returns all values even when indexed with [0] #1411", () => { + const sharedCode = ` + declare function select(this:void, index: '#', ...args: T[]): number; + + function foo(this: void): LuaMultiReturn<[number, number]> { + return $multi(123, 456); + } + + function bar(this: void): number { + return foo()[0]; + } + `; + test("Returns the correct value", () => { + util.testFunction` + return bar(); + ` + .setTsHeader(sharedCode) + .withLanguageExtensions() + .expectToEqual(123); + }); + + // We require an extra test since the test above would succeed even if bar() returned more than one value. + // This is because our test helper always returns just the first lua value returned from the testFunction. + // Here we need to test explicitly that only one value is returned. + test("Does not return multiple values", () => { + util.testFunction` + return select("#", bar()); + ` + .setTsHeader(sharedCode) + .withLanguageExtensions() + .expectToEqual(1); + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1591 +test("LuaMultiReturn in LuaIterable (#1591)", () => { + const lua = util.testModule` + type IterableAlias = LuaIterable>; + + declare const iterable: IterableAlias; + + for (const [a, b] of iterable) {} + ` + .withLanguageExtensions() + .getMainLuaCodeChunk(); + + expect(lua).toContain("a, b"); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1591 +test("LuaMultiReturn in LuaIterable intersection (#1591)", () => { + const lua = util.testModule` + declare function iterator(): { a: string } & LuaIterable>; + + for (const [a, b] of iterator()) {} + ` + .withLanguageExtensions() + .getMainLuaCodeChunk(); + + expect(lua).toContain("a, b"); +}); diff --git a/test/unit/language-extensions/operators.spec.ts b/test/unit/language-extensions/operators.spec.ts new file mode 100644 index 000000000..303a5d0be --- /dev/null +++ b/test/unit/language-extensions/operators.spec.ts @@ -0,0 +1,433 @@ +import * as path from "path"; +import * as util from "../../util"; +import * as tstl from "../../../src"; +import { LuaTarget } from "../../../src"; +import { + unsupportedForTarget, + invalidCallExtensionUse, + invalidSpreadInCallExtension, +} from "../../../src/transformation/utils/diagnostics"; + +const operatorsProjectOptions: tstl.CompilerOptions = { + luaTarget: LuaTarget.Lua54, + types: [path.resolve(__dirname, "../../../language-extensions")], +}; + +const operatorMapTestObject = ` + class OpTest { + public value: number; + public constructor(value: number) { this.value = value; } + + public __add(other: OpTest): OpTest; + public __add(other: number): number; + public __add(other: OpTest | number) { + if (typeof other === "number") { + return this.value + other; + } else { + return new OpTest(this.value + other.value); + } + } + public __sub(other: OpTest) { return new OpTest(this.value - other.value); } + public __mul(other: OpTest) { return new OpTest(this.value * other.value); } + public __div(other: OpTest) { return new OpTest(this.value / other.value); } + public __mod(other: OpTest) { return new OpTest(this.value % other.value); } + public __pow(other: OpTest) { return new OpTest(this.value ** other.value); } + public __idiv(other: OpTest) { return new OpTest(Math.floor(this.value / other.value)); } + public __band(other: OpTest) { return new OpTest(this.value & other.value); } + public __bor(other: OpTest) { return new OpTest(this.value | other.value); } + public __bxor(other: OpTest) { return new OpTest(this.value ^ other.value); } + public __shl(other: OpTest) { return new OpTest(this.value << other.value); } + public __shr(other: OpTest) { return new OpTest(this.value >>> other.value); } + + public __lt(other: OpTest) { return this.value < other.value; } + public __gt(other: OpTest) { return this.value > other.value; } + + public __concat(other: OpTest) { return this.value.toString() + other.value.toString(); } + + public __unm() { return -this.value; } + public __bnot() { return ~this.value; } + public __len() { return this.value; } + } +`; + +const binaryMathOperatorTests: Array<{ opType: string; left: number; right: number; expectResult: number }> = [ + { opType: "LuaAddition", left: 7, right: 4, expectResult: 11 }, + { opType: "LuaSubtraction", left: 7, right: 4, expectResult: 3 }, + { opType: "LuaMultiplication", left: 7, right: 4, expectResult: 28 }, + { opType: "LuaDivision", left: 7, right: 4, expectResult: 1.75 }, + { opType: "LuaModulo", left: 7, right: 4, expectResult: 3 }, + { opType: "LuaPower", left: 7, right: 4, expectResult: 2401 }, + { opType: "LuaFloorDivision", left: 7, right: 4, expectResult: 1 }, + { opType: "LuaBitwiseAnd", left: 6, right: 5, expectResult: 4 }, + { opType: "LuaBitwiseOr", left: 6, right: 5, expectResult: 7 }, + { opType: "LuaBitwiseExclusiveOr", left: 6, right: 5, expectResult: 3 }, + { opType: "LuaBitwiseLeftShift", left: 7, right: 2, expectResult: 28 }, + { opType: "LuaBitwiseRightShift", left: 7, right: 2, expectResult: 1 }, +]; + +test.each(binaryMathOperatorTests)( + "binary math operator mapping - global function (%s)", + ({ opType, left, right, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare const op: ${opType}; + const left = new OpTest(${left}); + const right = new OpTest(${right}); + export const result = op(left, right).value; + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); + } +); + +test.each(binaryMathOperatorTests)( + "binary math operator mapping - static function (%s)", + ({ opType, left, right, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare namespace OpTest { + export const op: ${opType}; + } + const left = new OpTest(${left}); + const right = new OpTest(${right}); + export const result = OpTest.op(left, right).value; + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); + } +); + +test.each(binaryMathOperatorTests)( + "binary math operator mapping - method (%s)", + ({ opType, left, right, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare interface OpTest { + op: ${opType}Method; + } + const left = new OpTest(${left}); + const right = new OpTest(${right}); + export const result = left.op(right).value; + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); + } +); + +const luaTargetsPre53 = [LuaTarget.Lua50, LuaTarget.Lua51, LuaTarget.Lua52, LuaTarget.LuaJIT, LuaTarget.Universal]; + +const operatorTypesPost53 = [ + "LuaFloorDivision", + "LuaBitwiseAnd", + "LuaBitwiseOr", + "LuaBitwiseExclusiveOr", + "LuaBitwiseLeftShift", + "LuaBitwiseRightShift", +]; + +test.each(luaTargetsPre53.flatMap(target => operatorTypesPost53.map(opType => [target, opType] as const)))( + "unsupported binary operator mapping (%s %s)", + (luaTarget, opType) => { + util.testModule` + declare interface OpTest { value: number; } + declare const op: ${opType}; + declare const left: OpTest; + declare const right: OpTest; + op(left, right); + ` + .setOptions({ ...operatorsProjectOptions, luaTarget }) + .expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]); + } +); + +const comparisonOperatorTests: Array<{ opType: string; left: number; right: number; expectResult: boolean }> = [ + { opType: "LuaLessThan", left: 7, right: 4, expectResult: false }, + { opType: "LuaLessThan", left: 4, right: 7, expectResult: true }, + { opType: "LuaGreaterThan", left: 7, right: 4, expectResult: true }, + { opType: "LuaGreaterThan", left: 4, right: 7, expectResult: false }, +]; + +test.each(comparisonOperatorTests)( + "comparison operator mapping - global function (%s)", + ({ opType, left, right, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare const op: ${opType}; + const left = new OpTest(${left}); + const right = new OpTest(${right}); + export const result = op(left, right); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); + } +); + +test.each(comparisonOperatorTests)( + "comparison operator mapping - static function (%s)", + ({ opType, left, right, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare namespace OpTest { + export const op: ${opType}; + } + const left = new OpTest(${left}); + const right = new OpTest(${right}); + export const result = OpTest.op(left, right); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); + } +); + +test.each(comparisonOperatorTests)( + "comparison operator mapping - method (%s)", + ({ opType, left, right, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare interface OpTest { + op: ${opType}Method; + } + const left = new OpTest(${left}); + const right = new OpTest(${right}); + export const result = left.op(right); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); + } +); + +test("concat operator mapping - global function", () => { + util.testModule` + ${operatorMapTestObject} + declare const op: LuaConcat; + const left = new OpTest(7); + const right = new OpTest(4); + export const result = op(left, right); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual("74"); +}); + +test("concat operator mapping - static function", () => { + util.testModule` + ${operatorMapTestObject} + declare namespace OpTest { + export const op: LuaConcat; + } + const left = new OpTest(7); + const right = new OpTest(4); + export const result = OpTest.op(left, right); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual("74"); +}); + +test("concat operator mapping - method", () => { + util.testModule` + ${operatorMapTestObject} + declare interface OpTest { + op: LuaConcatMethod; + } + const left = new OpTest(7); + const right = new OpTest(4); + export const result = left.op(right); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual("74"); +}); + +const unaryOperatorTests: Array<{ opType: string; operand: number; expectResult: number }> = [ + { opType: "LuaNegation", operand: 13, expectResult: -13 }, + { opType: "LuaBitwiseNot", operand: 13, expectResult: -14 }, + { opType: "LuaLength", operand: 13, expectResult: 13 }, +]; + +test.each(unaryOperatorTests)("unary operator mapping - global function (%s)", ({ opType, operand, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare const op: ${opType}; + const operand = new OpTest(${operand}); + export const result = op(operand); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); +}); + +test.each(unaryOperatorTests)("unary operator mapping - static function (%s)", ({ opType, operand, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare namespace OpTest { + export const op: ${opType}; + } + const operand = new OpTest(${operand}); + export const result = OpTest.op(operand); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); +}); + +test.each(unaryOperatorTests)("unary operator mapping - method (%s)", ({ opType, operand, expectResult }) => { + util.testModule` + ${operatorMapTestObject} + declare interface OpTest { + op: ${opType}Method; + } + const operand = new OpTest(${operand}); + export const result = operand.op(); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(expectResult); +}); + +test.each(luaTargetsPre53)("unsupported unary operator mapping (%s LuaBitwiseNot)", luaTarget => { + util.testModule` + declare interface OpTest { value: number; } + declare const op: LuaBitwiseNot; + declare const operand: OpTest; + op(operand); + ` + .setOptions({ ...operatorsProjectOptions, luaTarget }) + .expectDiagnosticsToMatchSnapshot([unsupportedForTarget.code]); +}); + +test("operator mapping overload - global function", () => { + util.testModule` + ${operatorMapTestObject} + declare const op: LuaAddition & LuaAddition; + const left = new OpTest(4); + const right = new OpTest(7); + const resultA = op(left, right).value; + const resultB = op(left, 13); + export const result = {resultA, resultB}; + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual({ resultA: 11, resultB: 17 }); +}); + +test("operator mapping overload - static function", () => { + util.testModule` + ${operatorMapTestObject} + declare namespace OpTest { + export const op: LuaAddition & LuaAddition; + } + const left = new OpTest(4); + const right = new OpTest(7); + const resultA = OpTest.op(left, right).value; + const resultB = OpTest.op(left, 13); + export const result = {resultA, resultB}; + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual({ resultA: 11, resultB: 17 }); +}); + +test("operator mapping overload - method", () => { + util.testModule` + ${operatorMapTestObject} + declare interface OpTest { + op: LuaAdditionMethod & LuaAdditionMethod; + } + const left = new OpTest(4); + const right = new OpTest(7); + const resultA = left.op(right).value; + const resultB = left.op(13); + export const result = {resultA, resultB}; + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual({ resultA: 11, resultB: 17 }); +}); + +test("operator mapping - method element call", () => { + util.testModule` + ${operatorMapTestObject} + declare interface OpTest { + add: LuaAdditionMethod; + } + const left = new OpTest(7); + export const result = left["add"](4); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(11); +}); + +test("operator mapping - function element call", () => { + util.testModule` + ${operatorMapTestObject} + declare namespace OpTest { + export const add: LuaAddition; + } + const left = new OpTest(7); + export const result = OpTest["add"](left, 4); + ` + .setOptions(operatorsProjectOptions) + .setReturnExport("result") + .expectToEqual(11); +}); + +test.each([ + "const foo: unknown = op;", + "const foo = `${op}`", + "declare function foo(op: LuaAddition): void; foo(op);", + "const foo = (op as any)(1, 2);", + "const foo = [op];", +])("operator mapping - invalid use (%s)", invalidCode => { + util.testModule` + declare const op: LuaAddition; + ${invalidCode} + ` + .setOptions(operatorsProjectOptions) + .expectDiagnosticsToMatchSnapshot([invalidCallExtensionUse.code]); +}); + +describe("does not crash on invalid operator use", () => { + test("global function", () => { + util.testModule` + declare const op: LuaAddition; + op(1) + ` + .setOptions(operatorsProjectOptions) + .expectDiagnosticsToMatchSnapshot(); + }); + test("unary operator", () => { + util.testModule` + declare const op: LuaUnaryMinus; + op() + ` + .setOptions(operatorsProjectOptions) + .expectDiagnosticsToMatchSnapshot(); + }); + test("method", () => { + util.testModule` + const left = {} as { + op: LuaAdditionMethod; + } + left.op() + ` + .setOptions(operatorsProjectOptions) + .expectDiagnosticsToMatchSnapshot(); + }); +}); + +test("does not allow spread", () => { + util.testModule` + declare const op: LuaAddition; + op(...[1, 2] as const); + ` + .setOptions(operatorsProjectOptions) + .expectToHaveDiagnostics([invalidSpreadInCallExtension.code]); +}); diff --git a/test/unit/language-extensions/pairsIterable.spec.ts b/test/unit/language-extensions/pairsIterable.spec.ts new file mode 100644 index 000000000..51c7e0b4e --- /dev/null +++ b/test/unit/language-extensions/pairsIterable.spec.ts @@ -0,0 +1,219 @@ +import * as util from "../../util"; +import { invalidPairsIterableWithoutDestructuring } from "../../../src/transformation/utils/diagnostics"; +import { LuaTarget } from "../../../src"; + +const testIterable = ` +const testIterable = {a1: "a2", b1: "b2", c1: "c2"} as unknown as LuaPairsIterable; +`; + +const testResults = { + a1: "a2", + b1: "b2", + c1: "c2", +}; + +test("pairs iterable", () => { + util.testFunction` + ${testIterable} + const results: Record = {}; + for (const [k, v] of testIterable) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); +}); + +test("pairs iterable with external control variable", () => { + util.testFunction` + ${testIterable} + const results: Record = {}; + let k: string, v: string; + for ([k, v] of testIterable) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); +}); + +test("pairs iterable function forward", () => { + util.testFunction` + ${testIterable} + function forward() { return testIterable; } + const results: Record = {}; + for (const [k, v] of forward()) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); +}); + +test("pairs iterable function indirect forward", () => { + util.testFunction` + ${testIterable} + function forward() { const iter = testIterable; return iter; } + const results: Record = {}; + for (const [k, v] of forward()) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); +}); + +test("pairs iterable arrow function forward", () => { + util.testFunction` + ${testIterable} + const forward = () => testIterable; + const results: Record = {}; + for (const [k, v] of forward()) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual(testResults); +}); + +test("pairs iterable with __pairs metamethod", () => { + util.testFunction` + class PairsTest { + __pairs() { + const kvp = [ ["a1", "a2"], ["b1", "b2"], ["c1", "c2"] ]; + let i = 0; + return () => { + if (i < kvp.length) { + const [k, v] = kvp[i++]; + return $multi(k, v); + } + }; + } + } + const tester = new PairsTest() as PairsTest & LuaPairsIterable; + const results: Record = {}; + for (const [k, v] of tester) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .setOptions({ luaTarget: LuaTarget.Lua53 }) + .expectToEqual(testResults); +}); + +test.each(["for (const s of testIterable) {}", "let s; for (s of testIterable) {}"])( + "invalid LuaPairsIterable without destructuring (%p)", + statement => { + util.testFunction` + ${testIterable} + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidPairsIterableWithoutDestructuring.code]); + } +); + +const testKeyIterable = ` +const testKeyIterable = {a1: true, b1: true, c1: true} as unknown as LuaPairsKeyIterable; +`; + +test("pairs key iterable", () => { + util.testFunction` + ${testKeyIterable} + const results: Record = {}; + for (const k of testKeyIterable) { + results[k] = true; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual({ a1: true, b1: true, c1: true }); +}); + +test("pairs key iterable with external control variable", () => { + util.testFunction` + ${testKeyIterable} + const results: Record = {}; + let k: string; + for (k of testKeyIterable) { + results[k] = true; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual({ a1: true, b1: true, c1: true }); +}); + +test("pairs key iterable function forward", () => { + util.testFunction` + ${testKeyIterable} + function forward() { return testKeyIterable; } + const results: Record = {}; + for (const k of forward()) { + results[k] = true; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual({ a1: true, b1: true, c1: true }); +}); + +test("pairs key iterable function indirect forward", () => { + util.testFunction` + ${testKeyIterable} + function forward() { const iter = testKeyIterable; return iter; } + const results: Record = {}; + for (const k of forward()) { + results[k] = true; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual({ a1: true, b1: true, c1: true }); +}); + +test("pairs key iterable arrow function forward", () => { + util.testFunction` + ${testKeyIterable} + const forward = () => testKeyIterable; + const results: Record = {}; + for (const k of forward()) { + results[k] = true; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual({ a1: true, b1: true, c1: true }); +}); + +test("pairs key iterable with __pairs metamethod", () => { + util.testFunction` + class PairsTest { + __pairs() { + const kvp = [ ["a1", true], ["b1", true], ["c1", true] ]; + let i = 0; + return () => { + if (i < kvp.length) { + const [k, v] = kvp[i++]; + return $multi(k, v); + } + }; + } + } + const tester = new PairsTest() as PairsTest & LuaPairsKeyIterable; + const results: Record = {}; + for (const k of tester) { + results[k] = true; + } + return results; + ` + .withLanguageExtensions() + .setOptions({ luaTarget: LuaTarget.Lua53 }) + .expectToEqual({ a1: true, b1: true, c1: true }); +}); diff --git a/test/unit/language-extensions/range.spec.ts b/test/unit/language-extensions/range.spec.ts new file mode 100644 index 000000000..3fc5539f6 --- /dev/null +++ b/test/unit/language-extensions/range.spec.ts @@ -0,0 +1,62 @@ +import * as util from "../../util"; +import { invalidRangeControlVariable, invalidRangeUse } from "../../../src/transformation/utils/diagnostics"; + +test("$range basic use", () => { + util.testFunction` + const result: number[] = []; + for (const i of $range(1, 5)) { + result.push(i); + } + return result; + ` + .withLanguageExtensions() + .expectToEqual([1, 2, 3, 4, 5]); +}); + +test("$range basic use with step", () => { + util.testFunction` + const result: number[] = []; + for (const i of $range(1, 10, 2)) { + result.push(i); + } + return result; + ` + .withLanguageExtensions() + .expectToEqual([1, 3, 5, 7, 9]); +}); + +test("$range basic use reverse", () => { + util.testFunction` + const result: number[] = []; + for (const i of $range(5, 1, -1)) { + result.push(i); + } + return result; + ` + .withLanguageExtensions() + .expectToEqual([5, 4, 3, 2, 1]); +}); + +test("$range invalid control variable", () => { + util.testFunction` + let i: number; + for (i of $range(1, 5)) {} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidRangeControlVariable.code]); +}); + +test.each([ + "for (const i in $range(1, 10, 2)) {}", + "const x = $range(1, 10);", + "const range = $range;", + "const y = [...$range(1, 10)];", + "for (const i of ($range(1, 10, 2))) {}", + "for (const i of $range(1, 10, 2) as number[]) {}", +])("$range invalid use (%p)", statement => { + util.testFunction` + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidRangeUse.code]); +}); diff --git a/test/unit/language-extensions/table.spec.ts b/test/unit/language-extensions/table.spec.ts new file mode 100644 index 000000000..13af766fa --- /dev/null +++ b/test/unit/language-extensions/table.spec.ts @@ -0,0 +1,539 @@ +import * as util from "../../util"; +import { invalidCallExtensionUse } from "../../../src/transformation/utils/diagnostics"; + +describe("LuaTableGet & LuaTableSet extensions", () => { + test("stand-alone function", () => { + util.testModule` + declare const getTable: LuaTableGet<{}, string, number>; + declare const setTable: LuaTableSet<{}, string, number>; + const tbl = {}; + setTable(tbl, "foo", 3); + const result = getTable(tbl, "foo"); + export { result } + ` + .withLanguageExtensions() + .setReturnExport("result") + .expectToEqual(3); + }); + + test("stand-alone function with preceding statements", () => { + util.testModule` + declare const setTable: LuaTableSet<{}, string, string>; + const values: any[] = [] + const tbl: any = {}; + const get = (value: any) => { + values.push(value) + return value + } + let x = "a" + setTable(tbl, x += "b", x += "c") + export const result = tbl.ab + ` + .withLanguageExtensions() + .setReturnExport("result") + .expectToEqual("abc"); + }); + + test("namespace function", () => { + util.testModule` + declare namespace Table { + export const get: LuaTableGet<{}, string, number>; + export const set: LuaTableSet<{}, string, number>; + } + const tbl = {}; + Table.set(tbl, "foo", 3); + const result = Table.get(tbl, "foo"); + export { result } + ` + .withLanguageExtensions() + .setReturnExport("result") + .expectToEqual(3); + }); + + test("method", () => { + util.testModule` + interface Table { + get: LuaTableGetMethod; + set: LuaTableSetMethod; + } + const tbl = {} as Table; + tbl.set("foo", 3); + const result = tbl.get("foo"); + export { result } + ` + .withLanguageExtensions() + .setReturnExport("result") + .expectToEqual(3); + }); + + test.each([ + "const foo: unknown = getTable;", + "const foo = `${getTable}`;", + "declare function foo(getTable: LuaTableGet<{}, string, number>): void; foo(getTable);", + "const foo = (getTable as any)(1, 2);", + "const foo = [getTable];", + ])("invalid use (%p)", statement => { + util.testModule` + declare const getTable: LuaTableGet<{}, string, number>; + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidCallExtensionUse.code]); + }); +}); + +describe("LuaTableHas extension", () => { + test("LuaTableHas standalone function", () => { + util.testModule` + declare const tableHas: LuaTableHas<{}, string>; + const table = { foo: "bar" }; + + export const hasFoo = tableHas(table, "foo"); + export const hasBaz = tableHas(table, "baz"); + ` + .withLanguageExtensions() + .expectToEqual({ hasFoo: true, hasBaz: false }); + }); + + test("LuaTableHas nested expression", () => { + util.testModule` + declare const tableHas: LuaTableHas<{}, string>; + const table = { foo: "bar" }; + + export const result = \`table has foo: \${tableHas(table, "foo")}\`; + ` + .withLanguageExtensions() + .expectToEqual({ result: "table has foo: true" }); + }); + + test("LuaTableHas namespace function", () => { + util.testModule` + declare namespace Table { + export const tableHas: LuaTableHas<{}, string>; + } + const table = { foo: "bar" }; + + export const hasFoo = Table.tableHas(table, "foo"); + export const hasBaz = Table.tableHas(table, "baz"); + ` + .withLanguageExtensions() + .expectToEqual({ hasFoo: true, hasBaz: false }); + }); + + test("LuaTableHasMethod method", () => { + util.testModule` + interface TableWithHas { + has: LuaTableHasMethod; + set: LuaTableSetMethod; + } + const table = {} as TableWithHas; + table.set("foo", 42); + + export const hasFoo = table.has("foo"); + export const hasBaz = table.has("baz"); + ` + .withLanguageExtensions() + .expectToEqual({ hasFoo: true, hasBaz: false }); + }); + + test("LuaTableHas as statement", () => { + util.testModule` + declare const tableHas: LuaTableHas<{}, string>; + const table = { foo: "bar" }; + + let count = 0; + function getKey() { + count++; + return "foo"; + } + + // The result is not captured, but the expression will be evaulated + tableHas(table, getKey()); + + export const numCalls = count; + ` + .withLanguageExtensions() + .expectToEqual({ numCalls: 1 }); + }); + + test.each([ + "const foo: unknown = tableHas;", + "const foo = `${tableHas}`;", + "declare function foo(tableHas: LuaTableHas<{}, string>): void; foo(tableHas);", + "const foo = (tableHas as any)(1, 2);", + "const foo = [tableHas];", + ])("invalid use (%p)", statement => { + util.testModule` + declare const tableHas: LuaTableHas<{}, string>; + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidCallExtensionUse.code]); + }); + + test.each(["LuaTable", "LuaMap", "LuaSet"])( + "invalid use method assignment (%p)", + type => { + util.testModule` + const table = new ${type}(); + const has = table.has; + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidCallExtensionUse.code]); + } + ); + + test.each(["LuaTable", "LuaMap", "LuaSet"])( + "invalid use method expression (%p)", + type => { + util.testModule` + const table = new ${type}(); + ["a", "b", "c"].map(table.has); + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidCallExtensionUse.code]); + } + ); +}); + +describe("LuaTableDelete extension", () => { + test("LuaTableDelete standalone function", () => { + util.testModule` + declare const tableDelete: LuaTableDelete<{}, string>; + + export const table = { foo: "bar", baz: "baz" }; + tableDelete(table, "foo"); + ` + .withLanguageExtensions() + .expectToEqual({ table: { baz: "baz" } }); + }); + + test("LuaTableDelete namespace function", () => { + util.testModule` + declare namespace Table { + export const tableDelete: LuaTableDelete<{}, string>; + } + + export const table = { foo: "bar", baz: "baz" }; + Table.tableDelete(table, "foo"); + ` + .withLanguageExtensions() + .expectToEqual({ table: { baz: "baz" } }); + }); + + test("LuaTableDeleteMethod method", () => { + util.testModule` + interface TableWithDelete { + delete: LuaTableDeleteMethod; + set: LuaTableSetMethod; + } + export const table = {} as TableWithDelete; + table.set("foo", 42); + table.set("bar", 12); + table.delete("foo"); + ` + .withLanguageExtensions() + .expectToEqual({ table: { bar: 12 } }); + }); +}); + +describe("LuaTableAddKey extension", () => { + test("LuaTableAddKey standalone function", () => { + util.testModule` + declare const tableAddKey: LuaTableAddKey<{}, string>; + export const table = { foo: "bar" }; + tableAddKey(table, "baz"); + ` + .withLanguageExtensions() + .expectToEqual({ table: { foo: "bar", baz: true } }); + }); + + test("LuaTableAddKey namespace function", () => { + util.testModule` + declare namespace Table { + export const tableAddKey: LuaTableAddKey<{}, string>; + } + export const table = { foo: "bar" }; + Table.tableAddKey(table, "baz"); + ` + .withLanguageExtensions() + .expectToEqual({ table: { foo: "bar", baz: true } }); + }); + + test("LuaTableAddKey method", () => { + util.testModule` + interface TableWithAddKey { + addKey: LuaTableAddKeyMethod; + } + export const table = {} as TableWithAddKey; + table.addKey("bar"); + ` + .withLanguageExtensions() + .expectToEqual({ table: { bar: true } }); + }); +}); + +describe("LuaIsEmpty extension", () => { + test("LuaIsEmpty standalone function", () => { + util.testModule` + declare const isTableEmpty: LuaTableIsEmpty<{}>; + + const table = { foo: "bar", baz: "baz" }; + const emptyTable = {}; + + export const result = [isTableEmpty(table), isTableEmpty(emptyTable)]; + ` + .withLanguageExtensions() + .expectToEqual({ result: [false, true] }); + }); + + test("LuaIsEmpty namespace function", () => { + util.testModule` + declare namespace Table { + export const isTableEmpty: LuaTableIsEmpty<{}>; + } + + const table = { foo: "bar", baz: "baz" }; + const emptyTable = {}; + + export const result = [Table.isTableEmpty(table), Table.isTableEmpty(emptyTable)]; + ` + .withLanguageExtensions() + .expectToEqual({ result: [false, true] }); + }); + + test("LuaTableIsEmptyMethod method", () => { + util.testModule` + interface TableWithIsEmpty { + isEmpty: LuaTableIsEmptyMethod; + set: LuaTableSetMethod; + } + const table = {} as TableWithIsEmpty; + table.set("foo", 42); + table.set("bar", 12); + + const emptyTable = {} as TableWithIsEmpty; + + export const result = [table.isEmpty(), emptyTable.isEmpty()]; + ` + .withLanguageExtensions() + .expectToEqual({ result: [false, true] }); + }); +}); + +describe("Table extensions use as expression", () => { + test.each([ + ["LuaTableAddKey<{}, string>", 'func({}, "foo")', undefined], + ["LuaTableAddKey<{}, string>", '"truthy" && func({}, "foo")', undefined], + ["LuaTableDelete<{}, string>", 'func({}, "foo")', true], + ["LuaTableDelete<{}, string>", '"truthy" && func({}, "foo")', true], + ["LuaTableSet<{}, string, number>", 'func({}, "foo", 3)', undefined], + ["LuaTableIsEmpty<{}>", "func({})", true], + ["LuaTableIsEmpty<{}>", 'func({ foo: "bar", baz: "baz" })', false], + ["LuaTableIsEmpty<{}>", '"truthy" && func({})', true], + ])("functions used as expression", (funcType, expression, value) => { + util.testModule` + declare const func: ${funcType} + export const result = ${expression} + ` + .withLanguageExtensions() + .setReturnExport("result") + .ignoreDiagnostics([2872 /* TS2872: This kind of expression is always truthy. */]) + .expectToEqual(value); + }); + + test.each([ + ["LuaTableDeleteMethod", 'tbl.func("foo")', true], + ["LuaTableSetMethod", 'tbl.func("foo", 3)', undefined], + ["LuaTableAddKeyMethod", 'tbl.func("foo")', undefined], + ["LuaTableIsEmpty<{}>", "tbl.func({})", true], + ])("methods used as expression", (funcType, expression, value) => { + util.testModule` + const tbl = {} as { func: ${funcType} } + export const result = ${expression} + ` + .withLanguageExtensions() + .setReturnExport("result") + .expectToEqual(value); + }); +}); + +describe("LuaTable extension interface", () => { + test("untyped table", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set("foo", 3); + return tbl.get("foo"); + ` + .withLanguageExtensions() + .expectToEqual(3); + }); + + test("typed table", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set("foo", 3); + return tbl.get("foo"); + ` + .withLanguageExtensions() + .expectToEqual(3); + }); + + // Test to catch issue from https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1033 + test("typed table in type declaration", () => { + util.testFunction` + function fill(tbl: LuaTable) { + tbl.set("foo", 3); + return tbl; + } + return fill(new LuaTable()).get("foo"); + ` + .withLanguageExtensions() + .expectToEqual(3); + }); + + test.each([["null"], ["undefined"], ["number | undefined"], ["string | null"], ["unknown"]])( + "LuaTable in strict mode does not accept key type that could be nil (%p)", + keyType => { + util.testExpression`new LuaTable<${keyType}, unknown>()` + .withLanguageExtensions() + .setOptions({ strict: true }) + .expectToHaveDiagnostics() + .expectDiagnosticsToMatchSnapshot(); + } + ); + + test("object keyed table", () => { + util.testFunction` + interface Key { keyStr: string } + const key: Key = {keyStr: "foo"}; + const tbl = new LuaTable(); + tbl.set(key, 3); + return tbl.get(key); + ` + .withLanguageExtensions() + .expectToEqual(3); + }); + + test("table length", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set(1, "foo"); + tbl.set(3, "bar"); + return tbl.length(); + ` + .withLanguageExtensions() + .expectToEqual(1); + }); + + test("table has", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set(3, "foo"); + return [tbl.has(1), tbl.has(3)]; + ` + .withLanguageExtensions() + .expectToEqual([false, true]); + }); + + test("table delete", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set("foo", 1); + tbl.set("bar", 3); + tbl.set("baz", 5); + tbl.delete("bar"); + tbl.delete("foo"); + return tbl; + ` + .withLanguageExtensions() + .expectToEqual({ baz: 5 }); + }); + + test("table isEmpty", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set("foo", 1); + + const emptyTbl = new LuaTable(); + + return [tbl.isEmpty(), emptyTbl.isEmpty()]; + ` + .withLanguageExtensions() + .expectToEqual([false, true]); + }); + + test("table add", () => { + util.testFunction` + const tbl = new LuaSet(); + tbl.add("foo"); + return tbl + ` + .withLanguageExtensions() + .expectToEqual({ foo: true }); + }); + + test.each(['new LuaTable().get("foo");', 'new LuaTable().set("foo", "bar");'])( + "table immediate access (%p)", + statement => { + util.testFunction(statement).withLanguageExtensions().expectToHaveNoDiagnostics(); + } + ); + + test("table pairs iterate", () => { + util.testFunction` + const tbl = new LuaTable(); + tbl.set("foo", 1); + tbl.set("bar", 3); + tbl.set("baz", 5); + const results: Record = {}; + for (const [k, v] of tbl) { + results[k] = v; + } + return results; + ` + .withLanguageExtensions() + .expectToEqual({ foo: 1, bar: 3, baz: 5 }); + }); +}); + +test.each([ + [undefined, undefined], + ["new LuaSet()", true], +])("call on optional table with strictNullChecks (%s)", (value, expected) => { + util.testFunction` + function getFoo(): LuaSet | undefined { + return ${value} + } + const foo = getFoo() + foo?.add("foo") + return foo?.has("foo") + ` + .setOptions({ + strictNullChecks: true, + }) + .withLanguageExtensions() + .expectToEqual(expected); +}); + +describe("does not crash on invalid extension use", () => { + test("global function", () => { + util.testModule` + declare const op: LuaTableGet<{}, string, any> + op({}) + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot(); + }); + + test("method", () => { + util.testModule` + const left = {} as { + op: LuaTableGet<{}, string, any> + } + left.op() + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot(); + }); +}); diff --git a/test/unit/language-extensions/vararg.spec.ts b/test/unit/language-extensions/vararg.spec.ts new file mode 100644 index 000000000..d5aa9fc69 --- /dev/null +++ b/test/unit/language-extensions/vararg.spec.ts @@ -0,0 +1,55 @@ +import * as util from "../../util"; +import { invalidVarargUse } from "../../../src/transformation/utils/diagnostics"; + +test.each([ + 'const result = [...$vararg].join("")', + 'let result: string; { result = [...$vararg].join(""); }', + 'let result: string; if (true) { result = [...$vararg].join(""); }', + 'let result: string; do { result = [...$vararg].join(""); } while (false);', + 'function foo(...args: unknown[]) { return args.join(""); } const result = foo(...$vararg);', +])("$vararg valid use (%p)", statement => { + util.testModule` + ${statement} + export { result }; + ` + .withLanguageExtensions() + .setLuaFactory(code => `return (function(...) ${code} end)("A", "B", "C", "D")`) + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toMatch("unpack")) + .expectToEqual({ result: "ABCD" }); +}); + +test.each([ + "const x = $vararg;", + "for (const x of $vararg) {}", + "const l = $vararg.length", + "function f(s: string[]) {} f($vararg);", + "function foo(...args: string[]) {} function bar() { foo(...$vararg); }", +])("$vararg invalid use (%p)", statement => { + util.testModule` + ${statement} + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot([invalidVarargUse.code]); +}); + +test("$vararg in bundle entry point", () => { + util.testModule` + export const result = [...$vararg].join(""); + ` + .setMainFileName("src/main.ts") + .setOptions({ rootDir: "src", luaBundle: "bundle.lua", luaBundleEntry: "src/main.ts" }) + .withLanguageExtensions() + .setLuaFactory(code => `return (function(...) ${code} end)("A", "B", "C", "D")`) + .expectToEqual({ result: "ABCD" }); +}); + +test("$vararg in bundle sub-module", () => { + util.testModule` + export { result } from "./module"; + ` + .setMainFileName("src/main.ts") + .addExtraFile("src/module.ts", 'export const result = [...$vararg].join("")') + .setOptions({ rootDir: "src", luaBundle: "bundle.lua", luaBundleEntry: "src/main.ts" }) + .withLanguageExtensions() + .expectToEqual({ result: "module" }); +}); diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 26eb44443..b88e0dbeb 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -1,313 +1,244 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as ts from "typescript"; -import { TranspileError } from "../../src/TranspileError"; -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; -import * as util from "../src/util"; - -const deepEqual = require("deep-equal"); - -export class LuaLoopTests -{ - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("while") - public while(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; - while (i < arrTest.length) { - arrTest[i] = arrTest[i] + 1; +import * as tstl from "../../src"; +import { LuaTarget } from "../../src"; +import { forbiddenForIn } from "../../src/transformation/utils/diagnostics"; +import * as util from "../util"; + +test("while", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + let i = 0; + while (i < arrTest.length) { + arrTest[i] = arrTest[i] + 1; + i++; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +util.testEachVersion( + "while with continue", + () => util.testFunction` + let arrTest = [0, 1, 2, 3, 4]; + let i = 0; + while (i < arrTest.length) { + if (i % 2 == 0) { i++; + continue; } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3, 4], [0, 1, 2, 1, 4]) - @Test("while with continue") - public whileWithContinue(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; - while (i < arrTest.length) { - if (i % 2 == 0) { - i++; + let j = 2; + while (j > 0) { + if (j == 2) { + j-- continue; } - let j = 2; - while (j > 0) { - if (j == 2) { - j-- - continue; - } - arrTest[i] = j; - j--; - } - + arrTest[i] = j; + j--; + } + + i++; + } + return arrTest; + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); + +util.testEachVersion( + "dowhile with continue", + () => util.testFunction` + let arrTest = [0, 1, 2, 3, 4]; + let i = 0; + do { + if (i % 2 == 0) { i++; + continue; } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3, 4], [0, 1, 2, 1, 4]) - @Test("dowhile with continue") - public dowhileWithContinue(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; + let j = 2; do { - if (i % 2 == 0) { - i++; + if (j == 2) { + j-- continue; } - let j = 2; - do { - if (j == 2) { - j-- - continue; - } - arrTest[i] = j; - j--; - } while (j > 0) - - i++; - } while (i < arrTest.length) - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("for") - public for(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - for (let i = 0; i < arrTest.length; ++i) { - arrTest[i] = arrTest[i] + 1; - } - return JSONStringify(arrTest);` - ); - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("for with expression") - public forWithExpression(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i: number; - for (i = 0 * 1; i < arrTest.length; ++i) { - arrTest[i] = arrTest[i] + 1; - } - return JSONStringify(arrTest);` - ); - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3, 4], [0, 0, 2, 0, 4]) - @Test("for with continue") - public forWithContinue(inp: number[], expected: number[]): void - { - // Transpile - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - for (let i = 0; i < arrTest.length; i++) { - if (i % 2 == 0) { + arrTest[i] = j; + j--; + } while (j > 0) + + i++; + } while (i < arrTest.length) + return arrTest; + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); + +test("for", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + for (let i = 0; i < arrTest.length; ++i) { + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +test("for with expression", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + let i: number; + for (i = 0 * 1; i < arrTest.length; ++i) { + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +util.testEachVersion( + "for with continue", + () => util.testFunction` + let arrTest = [0, 1, 2, 3, 4]; + for (let i = 0; i < arrTest.length; i++) { + if (i % 2 == 0) { + continue; + } + + for (let j = 0; j < 2; j++) { + if (j == 1) { continue; } - - for (let j = 0; j < 2; j++) { - if (j == 1) { - continue; - } - arrTest[i] = j; - } - } - return JSONStringify(arrTest); - ` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("forMirror") - public forMirror(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - for (let i = 0; arrTest.length > i; i++) { - arrTest[i] = arrTest[i] + 1; - } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [0, 1, 2, 3]) - @Test("forBreak") - public forBreak(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - for (let i = 0; i < arrTest.length; ++i) { + arrTest[i] = j; + } + } + return arrTest; + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); + +test("forMirror", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + for (let i = 0; arrTest.length > i; i++) { + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +test("forBreak", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + for (let i = 0; i < arrTest.length; ++i) { + break; + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +test("forNoDeclarations", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + let i = 0; + for (; i < arrTest.length; ++i) { + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +test("forNoCondition", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + let i = 0; + for (;; ++i) { + if (i >= arrTest.length) { break; - arrTest[i] = arrTest[i] + 1; - } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("forNoDeclarations") - public forNoDeclarations(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; - for (; i < arrTest.length; ++i) { - arrTest[i] = arrTest[i] + 1; } - return JSONStringify(arrTest);` - ); - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("forNoCondition") - public forNoCondition(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; - for (;; ++i) { - if (i >= arrTest.length) { - break; - } - - arrTest[i] = arrTest[i] + 1; - } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("forNoPostExpression") - public forNoPostExpression(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; - for (;;) { - if (i >= arrTest.length) { - break; - } - - arrTest[i] = arrTest[i] + 1; - - i++; - } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3], [1, 2, 3, 4], "let i = 0; i < arrTest.length; i++") - @TestCase([0, 1, 2, 3], [1, 2, 3, 4], "let i = 0; i <= arrTest.length - 1; i++") - @TestCase([0, 1, 2, 3], [1, 2, 3, 4], "let i = 0; arrTest.length > i; i++") - @TestCase([0, 1, 2, 3], [1, 2, 3, 4], "let i = 0; arrTest.length - 1 >= i; i++") - @TestCase([0, 1, 2, 3], [1, 1, 3, 3], "let i = 0; i < arrTest.length; i += 2") - @TestCase([0, 1, 2, 3], [1, 2, 3, 4 ], "let i = arrTest.length - 1; i >= 0; i--") - @TestCase([0, 1, 2, 3], [0, 2, 2, 4], "let i = arrTest.length - 1; i >= 0; i -= 2") - @TestCase([0, 1, 2, 3], [0, 2, 2, 4], "let i = arrTest.length - 1; i > 0; i -= 2") - @Test("forheader") - public forheader(inp: number[], expected: number[], header: string): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - for (${header}) { - arrTest[i] = arrTest[i] + 1; - } - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase("for scope") - public forScope(): void { - const code = - `let i = 42; - for (let i = 0; i < 10; ++i) {} - return i;`; - Expect(util.transpileAndExecute(code)).toBe(42); - } + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); - @TestCase({ ["test1"]: 0, ["test2"]: 1, ["test3"]: 2 }, { ["test1"]: 1, ["test2"]: 2, ["test3"]: 3 }) - @Test("forin[Object]") - public forinObject(inp: any, expected: any): void - { - const result = util.transpileAndExecute( - `let objTest = ${JSON.stringify(inp)}; - for (let key in objTest) { - objTest[key] = objTest[key] + 1; +test("forNoPostExpression", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + let i = 0; + for (;;) { + if (i >= arrTest.length) { + break; } - return JSONStringify(objTest);` - ); - - // Assert - Expect(deepEqual(JSON.parse(result), expected)).toBe(true); - } - - @TestCase([1, 2, 3]) - @Test("forin[Array]") - public forinArray(inp: number[]): void { - // Transpile & Assert - Expect(() => - util.transpileString( - `let arrTest = ${JSON.stringify(inp)}; - for (let key in arrTest) { - arrTest[key]++; - }` - ) - ).toThrowError(TranspileError, "Iterating over arrays with 'for ... in' is not allowed."); - } - @TestCase({a: 0, b: 1, c: 2, d: 3, e: 4}, {a: 0, b: 0, c: 2, d: 0, e: 4}) - @Test("forin with continue") - public forinWithContinue(inp: number[], expected: number[]): void + arrTest[i] = arrTest[i] + 1; + + i++; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +test.each([ + { inp: [0, 1, 2, 3], header: "let i = 0; i < arrTest.length; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; i <= arrTest.length - 1; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; arrTest.length > i; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; arrTest.length - 1 >= i; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; i < arrTest.length; i += 2" }, + { inp: [0, 1, 2, 3], header: "let i = arrTest.length - 1; i >= 0; i--" }, + { inp: [0, 1, 2, 3], header: "let i = arrTest.length - 1; i >= 0; i -= 2" }, + { inp: [0, 1, 2, 3], header: "let i = arrTest.length - 1; i > 0; i -= 2" }, +])("forheader (%p)", ({ inp, header }) => { + util.testFunction` + let arrTest = ${util.formatCode(inp)}; + for (${header}) { + arrTest[i] = arrTest[i] + 1; + } + return arrTest; + `.expectToMatchJsResult(); +}); + +test("for scope", () => { + util.testFunction` + let i = 42; + for (let i = 0; i < 10; ++i) {} + return i; + `.expectToMatchJsResult(); +}); + +test.each([ { - const result = util.transpileAndExecute( - `let obj = ${JSON.stringify(inp)}; + inp: { test1: 0, test2: 1, test3: 2 }, + }, +])("forin[Object] (%p)", ({ inp }) => { + util.testFunctionTemplate` + let objTest = ${inp}; + for (let key in objTest) { + objTest[key] = objTest[key] + 1; + } + return objTest; + `.expectToMatchJsResult(); +}); + +test("forin[Array]", () => { + util.testFunction` + const array = []; + for (const key in array) {} + `.expectDiagnosticsToMatchSnapshot([forbiddenForIn.code]); +}); + +const luaTargetsExceptJit = [ + LuaTarget.Lua50, + LuaTarget.Lua51, + LuaTarget.Lua52, + LuaTarget.Lua53, + LuaTarget.Lua54, + LuaTarget.Universal, +]; + +test.each( + luaTargetsExceptJit.flatMap(target => + [{ inp: { a: 0, b: 1, c: 2, d: 3, e: 4 } }].map(testCase => [target, testCase] as const) + ) +)("forin with continue (%s %p)", (luaTarget, { inp }) => { + util.testFunctionTemplate` + let obj = ${inp}; for (let i in obj) { if (obj[i] % 2 == 0) { continue; @@ -315,484 +246,405 @@ export class LuaLoopTests obj[i] = 0; } - return JSONStringify(obj); - ` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2], [1, 2, 3]) - @Test("forof") - public forof(inp: any, expected: any): void - { - const result = util.transpileAndExecute( - `let objTest = ${JSON.stringify(inp)}; - let arrResultTest = []; - for (let value of objTest) { - arrResultTest.push(value + 1) - } - return JSONStringify(arrResultTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2], [1, 2, 3]) - @Test("forof existing variable") - public forofExistingVar(inp: any, expected: any): void - { - const result = util.transpileAndExecute( - `let objTest = ${JSON.stringify(inp)}; - let arrResultTest = []; - let value: number; - for (value of objTest) { - arrResultTest.push(value + 1) - } - return JSONStringify(arrResultTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([[1, 2], [2, 3], [3, 4]], [3, 5, 7]) - @Test("forof destructing") - public forofDestructing(inp: number[][], expected: any): void - { - const result = util.transpileAndExecute( - `let objTest = ${JSON.stringify(inp)}; - let arrResultTest = []; - for (let [a,b] of objTest) { - arrResultTest.push(a + b) - } - return JSONStringify(arrResultTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([[1, 2], [2, 3], [3, 4]], [3, 5, 7]) - @Test("forof destructing with existing variables") - public forofDestructingExistingVars(inp: number[][], expected: any): void - { - const result = util.transpileAndExecute( - `let objTest = ${JSON.stringify(inp)}; - let arrResultTest = []; - let a: number; - let b: number; - for ([a,b] of objTest) { - arrResultTest.push(a + b) - } - return JSONStringify(arrResultTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([0, 1, 2, 3, 4], [0, 0, 2, 0, 4]) - @Test("forof with continue") - public forofWithContinue(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let testArr = ${JSON.stringify(inp)}; - let a = 0; - for (let i of testArr) { - if (i % 2 == 0) { - a++; - continue; - } - - for (let j of [0, 1]) { - if (j == 1) { - continue; - } - testArr[a] = j; - } + return obj; + ` + .setOptions({ luaTarget }) + .expectToMatchJsResult(); +}); + +test.each([{ inp: [0, 1, 2] }])("forof (%p)", ({ inp }) => { + util.testFunctionTemplate` + let objTest = ${inp}; + let arrResultTest = []; + for (let value of objTest) { + arrResultTest.push(value + 1) + } + return arrResultTest; + `.expectToMatchJsResult(); +}); + +test("Tuple loop", () => { + util.testFunction` + const tuple: [number, number, number] = [3,5,1]; + let count = 0; + for (const value of tuple) { count += value; } + return count; + `.expectToMatchJsResult(); +}); + +test("forof existing variable", () => { + util.testFunction` + let objTest = [0, 1, 2]; + let arrResultTest = []; + let value: number; + for (value of objTest) { + arrResultTest.push(value + 1) + } + return arrResultTest; + `.expectToMatchJsResult(); +}); + +test("forof destructing", () => { + const input = [ + [1, 2], + [2, 3], + [3, 4], + ]; + + util.testFunction` + let objTest = ${util.formatCode(input)}; + let arrResultTest = []; + for (let [a,b] of objTest) { + arrResultTest.push(a + b) + } + return arrResultTest; + `.expectToMatchJsResult(); +}); + +test("forof destructing scope", () => { + util.testFunction` + let x = 7; + for (let [x] of [[1], [2], [3]]) { + x *= 2; + } + return x; + `.expectToMatchJsResult(); +}); + +// This catches the case where x is falsely seen as globally scoped and the 'local' is stripped out +test("forof destructing scope (global)", () => { + util.testModule` + let x = 7; + for (let [x] of [[1], [2], [3]]) { + x *= 2; + } + if (x !== 7) throw x; + `.expectNoExecutionError(); +}); + +test("forof nested destructing", () => { + util.testFunction` + const obj = { a: [3], b: [5] }; + let result = 0; + + for(const [k, [v]] of Object.entries(obj)){ + result += v; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof destructing with existing variables", () => { + const input = [ + [1, 2], + [2, 3], + [3, 4], + ]; + + util.testFunction` + let objTest = ${util.formatCode(input)}; + let arrResultTest = []; + let a: number; + let b: number; + for ([a,b] of objTest) { + arrResultTest.push(a + b) + } + return arrResultTest; + `.expectToMatchJsResult(); +}); + +util.testEachVersion( + "forof with continue", + () => util.testFunction` + let testArr = [0, 1, 2, 3, 4]; + let a = 0; + for (let i of testArr) { + if (i % 2 == 0) { a++; + continue; } - return JSONStringify(testArr);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - @Test("forof with iterator") - public forofWithIterator(): void { - const code = `const arr = ["a", "b", "c"]; - function iter(): IterableIterator { - let i = 0; - return { - [Symbol.iterator]() { return this; }, - next() { return {value: arr[i], done: i++ >= arr.length} }, - } - } - let result = ""; - for (let e of iter()) { - result += e; - } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("abc"); - } - - @Test("forof with iterator and existing variable") - public forofWithIteratorExistingVar(): void { - const code = `const arr = ["a", "b", "c"]; - function iter(): IterableIterator { - let i = 0; - return { - [Symbol.iterator]() { return this; }, - next() { return {value: arr[i], done: i++ >= arr.length} }, + for (let j of [0, 1]) { + if (j == 1) { + continue; } - } - let result = ""; - let e: string; - for (e of iter()) { - result += e; - } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("abc"); - } - - @Test("forof destructuring with iterator") - public forofDestructuringWithIterator(): void { - const code = `const arr = ["a", "b", "c"]; - function iter(): IterableIterator<[string, string]> { + testArr[a] = j; + } + a++; + } + return testArr; + `, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) +); + +test("forof with iterator", () => { + util.testFunction` + const arr = ["a", "b", "c"]; + function iter(): IterableIterator { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: arr[i], done: i++ >= arr.length} }, + } + } + let result = ""; + for (let e of iter()) { + result += e; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof with iterator and existing variable", () => { + util.testFunction` + const arr = ["a", "b", "c"]; + function iter(): IterableIterator { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: arr[i], done: i++ >= arr.length} }, + } + } + let result = ""; + let e: string; + for (e of iter()) { + result += e; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof destructuring with iterator", () => { + util.testFunction` + const arr = ["a", "b", "c"]; + function iter(): IterableIterator<[string, string]> { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: [i.toString(), arr[i]], done: i++ >= arr.length} }, + } + } + let result = ""; + for (let [a, b] of iter()) { + result += a + b; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof destructuring with iterator and existing variables", () => { + util.testFunction` + const arr = ["a", "b", "c"]; + function iter(): IterableIterator<[string, string]> { + let i = 0; + return { + [Symbol.iterator]() { return this; }, + next() { return {value: [i.toString(), arr[i]], done: i++ >= arr.length} }, + } + } + let result = ""; + let a: string; + let b: string; + for ([a, b] of iter()) { + result += a + b; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof array which modifies length", () => { + util.testFunction` + const arr = ["a", "b", "c"]; + let result = ""; + for (const v of arr) { + if (v === "a") { + arr.push("d"); + } + result += v; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof nested destructuring", () => { + util.testFunction` + let result = 0; + for (const [[x]] of [[[1]]]) { + result = x; + } + return result; + `.expectToMatchJsResult(); +}); + +test("forof with array typed as iterable", () => { + util.testFunction` + function foo(): Iterable { + return ["A", "B", "C"]; + } + let result = ""; + for (const x of foo()) { + result += x; + } + return result; + `.expectToMatchJsResult(); +}); + +test.each(["", "abc", "a\0c"])("forof string (%p)", string => { + util.testFunctionTemplate` + const results: string[] = []; + for (const x of ${string}) { + results.push(x); + } + return results; + `.expectToMatchJsResult(); +}); + +describe("for...of empty destructuring", () => { + const declareTests = (destructuringPrefix: string) => { + test("array", () => { + util.testFunction` + const arr = [["a"], ["b"], ["c"]]; let i = 0; - return { - [Symbol.iterator]() { return this; }, - next() { return {value: [i.toString(), arr[i]], done: i++ >= arr.length} }, + for (${destructuringPrefix}[] of arr) { + ++i; } - } - let result = ""; - for (let [a, b] of iter()) { - result += a + b; - } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } + return i; + `.expectToMatchJsResult(); + }); - @Test("forof destructuring with iterator and existing variables") - public forofDestructuringWithIteratorExistingVars(): void { - const code = `const arr = ["a", "b", "c"]; - function iter(): IterableIterator<[string, string]> { + test("iterable", () => { + util.testFunction` + const iter: Iterable = [["a"], ["b"], ["c"]]; let i = 0; - return { - [Symbol.iterator]() { return this; }, - next() { return {value: [i.toString(), arr[i]], done: i++ >= arr.length} }, + for (${destructuringPrefix}[] of iter) { + ++i; } - } - let result = ""; - let a: string; - let b: string; - for ([a, b] of iter()) { - result += a + b; - } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } - - @Test("forof lua iterator") - public forofLuaIterator(): void { - const code = `const arr = ["a", "b", "c"]; - /** @luaIterator */ - function luaIter(): Iterable { - let i = 0; - return (() => arr[i++]) as any; - } - let result = ""; - for (let e of luaIter()) { result += e; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("abc"); - } - - @Test("forof lua iterator with existing variable") - public forofLuaIteratorExistingVar(): void { - const code = `const arr = ["a", "b", "c"]; - /** @luaIterator */ - function luaIter(): Iterable { - let i = 0; - return (() => arr[i++]) as any; - } - let result = ""; - let e: string; - for (e of luaIter()) { result += e; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("abc"); - } - - @Test("forof lua iterator destructuring") - public forofLuaIteratorDestructuring(): void { - const code = `const arr = ["a", "b", "c"]; - /** @luaIterator */ - function luaIter(): Iterable<[string, string]> { - let i = 0; - return (() => arr[i] && [i.toString(), arr[i++]]) as any; - } - let result = ""; - for (let [a, b] of luaIter()) { result += a + b; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } - - @Test("forof lua iterator destructuring with existing variables") - public forofLuaIteratorDestructuringExistingVar(): void { - const code = `const arr = ["a", "b", "c"]; - /** @luaIterator */ - function luaIter(): Iterable<[string, string]> { - let i = 0; - return (() => arr[i] && [i.toString(), arr[i++]]) as any; - } - let result = ""; - let a: string; - let b: string; - for ([a, b] of luaIter()) { result += a + b; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } - - @Test("forof lua iterator tuple-return") - public forofLuaIteratorTupleReturn(): void { - const code = `const arr = ["a", "b", "c"]; - /** @luaIterator */ - /** @tupleReturn */ - function luaIter(): Iterable<[string, string]> { - let i = 0; - /** @tupleReturn */ - function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } - return iter as any; - } - let result = ""; - for (let [a, b] of luaIter()) { result += a + b; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } - - @Test("forof lua iterator tuple-return with existing variables") - public forofLuaIteratorTupleReturnExistingVars(): void { - const code = `const arr = ["a", "b", "c"]; - /** @luaIterator */ - /** @tupleReturn */ - function luaIter(): Iterable<[string, string]> { - let i = 0; - /** @tupleReturn */ - function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } - return iter as any; - } - let result = ""; - let a: string; - let b: string; - for ([a, b] of luaIter()) { result += a + b; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } - - @Test("forof lua iterator tuple-return single variable") - public forofLuaIteratorTupleReturnSingleVar(): void { - const code = `/** @luaIterator */ - /** @tupleReturn */ - declare function luaIter(): Iterable<[string, string]>; - for (let x of luaIter()) {}`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - Expect(() => util.transpileString(code, compilerOptions)).toThrowError( - TranspileError, - "Unsupported use of lua iterator with TupleReturn decorator in for...of statement. " - + "You must use a destructuring statement to catch results from a lua iterator with " - + "the TupleReturn decorator."); - } - - @Test("forof lua iterator tuple-return single existing variable") - public forofLuaIteratorTupleReturnSingleExistingVar(): void { - const code = `/** @luaIterator */ - /** @tupleReturn */ - declare function luaIter(): Iterable<[string, string]>; - let x: [string, string]; - for (x of luaIter()) {}`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - Expect(() => util.transpileString(code, compilerOptions)).toThrowError( - TranspileError, - "Unsupported use of lua iterator with TupleReturn decorator in for...of statement. " - + "You must use a destructuring statement to catch results from a lua iterator with " - + "the TupleReturn decorator."); - } - - @Test("forof forwarded lua iterator") - public forofForwardedLuaIterator(): void { - const code = - `const arr = ["a", "b", "c"]; - /** @luaIterator */ - function luaIter(): Iterable { - let i = 0; - function iter() { return arr[i++]; } - return iter as any; - } - /** @luaIterator */ - function forward(): Iterable { - return luaIter(); - } - let result = ""; - for (let a of forward()) { result += a; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("abc"); - } - - @Test("forof forwarded lua iterator with tupleReturn") - public forofForwardedLuaIteratorWithTupleReturn(): void { - const code = - `const arr = ["a", "b", "c"]; - /** @luaIterator */ - /** @tupleReturn */ - function luaIter(): Iterable<[string, string]> { - let i = 0; - /** @tupleReturn */ - function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } - return iter as any; - } - /** @luaIterator */ - /** @tupleReturn */ - function forward(): Iterable<[string, string]> { - return luaIter(); - } - let result = ""; - for (let [a, b] of forward()) { result += a + b; } - return result;`; - const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - Expect(result).toBe("0a1b2c"); - } - - @TestCase("while (a < b) { i++; }") - @TestCase("do { i++; } while (a < b)") - @TestCase("for (let i = 0; i < 3; i++) {}") - @TestCase("for (let a in b) {}") - @TestCase("for (let a of b) {}") - @Test("loop versions") - public whileVersions(loop: string): void { - // Transpile - const lua51 = util.transpileString(loop, { luaTarget: LuaTarget.Lua51 }); - const lua52 = util.transpileString(loop, { luaTarget: LuaTarget.Lua52 }); - const lua53 = util.transpileString(loop, { luaTarget: LuaTarget.Lua53 }); - const luajit = util.transpileString(loop, { luaTarget: LuaTarget.LuaJIT }); - - // Assert - Expect(lua51.indexOf("::__continue1::") !== -1).toBe(false); // No labels in 5.1 - Expect(lua52.indexOf("::__continue1::") !== -1).toBe(true); // Labels from 5.2 onwards - Expect(lua53.indexOf("::__continue1::") !== -1).toBe(true); - Expect(luajit.indexOf("::__continue1::") !== -1).toBe(true); - } - - @Test("for dead code after return") - public forDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `for (let i = 0; i < 10; i++) { return 3; const b = 8; }`); - - Expect(result).toBe(3); - } - - @Test("for..in dead code after return") - public forInDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `for (let a in {"a": 5, "b": 8}) { return 3; const b = 8; }`); - - Expect(result).toBe(3); - } - - @Test("for..of dead code after return") - public forOfDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `for (let a of [1,2,4]) { return 3; const b = 8; }`); - - Expect(result).toBe(3); - } - - @Test("while dead code after return") - public whileDeadCodeAfterReturn(): void { - const result = util.transpileAndExecute( - `while (true) { return 3; const b = 8; }`); - - Expect(result).toBe(3); - } + return i; + `.expectToMatchJsResult(); + }); + }; + + describe("declaration", () => { + declareTests("const "); + }); + describe("assignment", () => { + declareTests(""); + }); +}); + +for (const testCase of [ + "while (false) { continue; }", + "do { continue; } while (false)", + "for (;;) { continue; }", + "for (const a in {}) { continue; }", + "for (const a of []) { continue; }", +]) { + const expectContinueVariable: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).toMatch(/local __continue\d+/); + + const expectContinueGotoLabel: util.TapCallback = builder => + 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, + [tstl.LuaTarget.Lua50]: expectContinueVariable, + [tstl.LuaTarget.Lua51]: expectContinueVariable, + [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; + do { + ++result; + } while (result < 2); + return result; + `.expectToMatchJsResult(); +}); + +test("do...while scoping", () => { + util.testFunction` + let x = 0; + let result = 0; + do { + let x = 1; + ++result; + } while (x === 0 && result < 2); + return result; + `.expectToMatchJsResult(); +}); + +test("do...while double-negation", () => { + const builder = util.testFunction` + let result = 0; + do { + ++result; + } while (!(result >= 2)); + return result; + `.expectToMatchJsResult(); + + expect(builder.getMainLuaCodeChunk()).not.toMatch("not"); +}); + +test("for...in with pre-defined variable", () => { + const testBuilder = util.testFunction` + const obj = { x: "y", foo: "bar" }; + + let x = ""; + let result = []; + for (x in obj) { + result.push(x); + } + return result; + `; + // Need custom matcher because order is not guaranteed in neither JS nor Lua + expect(testBuilder.getJsExecutionResult()).toEqual(expect.arrayContaining(testBuilder.getLuaExecutionResult())); +}); + +test("for...in with pre-defined variable keeps last value", () => { + const keyX = "x"; + const keyFoo = "foo"; + + const result = util.testFunction` + const obj = { ${keyX}: "y", ${keyFoo}: "bar" }; + + let x = ""; + for (x in obj) { + } + return x; + `.getLuaExecutionResult(); + // 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/lualib/inlining.spec.ts b/test/unit/lualib/inlining.spec.ts deleted file mode 100644 index 89aed54a9..000000000 --- a/test/unit/lualib/inlining.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Expect, Test } from "alsatian"; -import * as util from "../../src/util"; - -import { LuaLibImportKind, LuaTarget } from "../../../src/CompilerOptions"; - -export class InliningTests { - @Test("map constructor") - public mapConstructor(): void - { - const result = util.transpileAndExecute(`let mymap = new Map(); return mymap.size;`, - { luaLibImport: LuaLibImportKind.Inline, luaTarget: LuaTarget.Lua53 }); - - Expect(result).toBe(0); - } - - @Test("map foreach keys") - public mapForEachKeys(): void { - const result = util.transpileAndExecute( - `let mymap = new Map([[5, 2],[6, 3],[7, 4]]); - let count = 0; - mymap.forEach((value, key) => { count += key; }); - return count;`, - { luaLibImport: LuaLibImportKind.Inline, luaTarget: LuaTarget.Lua53 }); - - Expect(result).toBe(18); - } - - @Test("set constructor") - public setConstructor(): void - { - const result = util.transpileAndExecute( - `class abc {} let def = new abc(); let myset = new Set(); return myset.size;`, - { luaLibImport: LuaLibImportKind.Inline, luaTarget: LuaTarget.Lua53 } - ); - - Expect(result).toBe(0); - } - - @Test("set foreach keys") - public setForEachKeys(): void { - const result = util.transpileAndExecute( - `let myset = new Set([2, 3, 4]); - let count = 0; - myset.forEach((value, key) => { count += key; }); - return count;`, - { luaLibImport: LuaLibImportKind.Inline, luaTarget: LuaTarget.Lua53 }); - - Expect(result).toBe(9); - } -} diff --git a/test/unit/lualib/lualib.spec.ts b/test/unit/lualib/lualib.spec.ts deleted file mode 100644 index be87d1084..000000000 --- a/test/unit/lualib/lualib.spec.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as ts from "typescript"; -import * as util from "../../src/util"; -import { LuaLibImportKind } from "../../../src/CompilerOptions"; - -export class LuaLibTests -{ - @TestCase([0, 1, 2, 3], [1, 2, 3, 4]) - @Test("forEach") - public forEach(inp: number[], expected: number[]): void - { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - arrTest.forEach((elem, index) => { - arrTest[index] = arrTest[index] + 1; - }) - return JSONStringify(arrTest);` - ); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase([], "x => x") - @TestCase([0, 1, 2, 3], "x => x") - @TestCase([0, 1, 2, 3], "x => x*2") - @TestCase([1, 2, 3, 4], "x => -x") - @TestCase([0, 1, 2, 3], "x => x+2") - @TestCase([0, 1, 2, 3], "x => x%2 == 0 ? x + 1 : x - 1") - @Test("array.map") - public map(inp: T[], func: string): void - { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].map(${func}))`); - - // Assert - Expect(result).toBe(JSON.stringify(inp.map(eval(func)))); - } - - @TestCase([], "x => x > 1") - @TestCase([0, 1, 2, 3], "x => x > 1") - @TestCase([0, 1, 2, 3], "x => x < 3") - @TestCase([0, 1, 2, 3], "x => x < 0") - @TestCase([0, -1, -2, -3], "x => x < 0") - @TestCase([0, 1, 2, 3], "() => true") - @TestCase([0, 1, 2, 3], "() => false") - @Test("array.filter") - public filter(inp: T[], func: string): void - { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].filter(${func}))`); - - // Assert - Expect(result).toBe(JSON.stringify(inp.filter(eval(func)))); - } - - @TestCase([], "x => x > 1") - @TestCase([0, 1, 2, 3], "x => x > 1") - @TestCase([false, true, false], "x => x") - @TestCase([true, true, true], "x => x") - @Test("array.every") - public every(inp: T[], func: string): void - { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].every(${func}))`); - - // Assert - Expect(result).toBe(JSON.stringify(inp.every(eval(func)))); - } - - @TestCase([], "x => x > 1") - @TestCase([0, 1, 2, 3], "x => x > 1") - @TestCase([false, true, false], "x => x") - @TestCase([true, true, true], "x => x") - @Test("array.some") - public some(inp: T[], func: string): void - { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].some(${func}))`); - - // Assert - Expect(result).toBe(JSON.stringify(inp.some(eval(func)))); - } - - @TestCase([], 1, 2) - @TestCase([0, 1, 2, 3], 1, 2) - @TestCase([0, 1, 2, 3], 1, 1) - @TestCase([0, 1, 2, 3], 1, -1) - @TestCase([0, 1, 2, 3], -3, -1) - @TestCase([0, 1, 2, 3, 4, 5], 1, 3) - @TestCase([0, 1, 2, 3, 4, 5], 3) - @Test("array.slice") - public slice(inp: T[], start: number, end?: number): void - { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].slice(${start}, ${end}))`); - - // Assert - Expect(result).toBe(JSON.stringify(inp.slice(start, end))); - } - - @TestCase([], 0, 0, 9, 10, 11) - @TestCase([0, 1, 2, 3], 1, 0, 9, 10, 11) - @TestCase([0, 1, 2, 3], 2, 2, 9, 10, 11) - @TestCase([0, 1, 2, 3], 4, 1, 8, 9) - @TestCase([0, 1, 2, 3], 4, 0, 8, 9) - @TestCase([0, 1, 2, 3, 4, 5], 5, 9, 10, 11) - @TestCase([0, 1, 2, 3, 4, 5], 3, 2, 3, 4, 5) - @Test("array.splice[Insert]") - public spliceInsert(inp: T[], start: number, deleteCount: number, ...newElements: any[]): void - { - const result = util.transpileAndExecute( - `let spliceTestTable = [${inp.toString()}]; - spliceTestTable.splice(${start}, ${deleteCount}, ${newElements}); - return JSONStringify(spliceTestTable);` - ); - - // Assert - inp.splice(start, deleteCount, ...newElements); - Expect(result).toBe(JSON.stringify(inp)); - } - - @TestCase([], 1, 1) - @TestCase([0, 1, 2, 3], 1, 1) - @TestCase([0, 1, 2, 3], 10, 1) - @TestCase([0, 1, 2, 3], 4) - @TestCase([0, 1, 2, 3, 4, 5], 3) - @TestCase([0, 1, 2, 3, 4, 5], 2, 2) - @TestCase([0, 1, 2, 3, 4, 5, 6, 7, 8], 5, 9, 10, 11) - @Test("array.splice[Remove]") - public spliceRemove(inp: T[], start: number, deleteCount?: number, ...newElements: any[]): void - { - let result; - if (deleteCount) { - result = util.transpileAndExecute( - `let spliceTestTable = [${inp.toString()}]; - spliceTestTable.splice(${start}, ${deleteCount}, ${newElements}); - return JSONStringify(spliceTestTable);` - ); - } else { - result = util.transpileAndExecute( - `let spliceTestTable = [${inp.toString()}]; - spliceTestTable.splice(${start}); - return JSONStringify(spliceTestTable);` - ); - } - - // Assert - if (deleteCount) { - inp.splice(start, deleteCount, ...newElements); - Expect(result).toBe(JSON.stringify(inp)); - } else { - inp.splice(start); - Expect(result).toBe(JSON.stringify(inp)); - } - } - - @TestCase([], []) - @TestCase([1, 2, 3], []) - @TestCase([1, 2, 3], [4]) - @TestCase([1, 2, 3], [4, 5]) - @TestCase([1, 2, 3], [4, 5]) - @TestCase([1, 2, 3], 4, [5]) - @TestCase([1, 2, 3], 4, [5, 6]) - @TestCase([1, 2, 3], 4, [5, 6], 7) - @TestCase([1, 2, 3], "test", [5, 6], 7, ["test1", "test2"]) - @TestCase([1, 2, "test"], "test", ["test1", "test2"]) - @Test("array.concat") - public concat(arr: T[], ...args: T[]): void - { - const argStr = args.map(arg => JSON.stringify(arg)).join(","); - - const result = util.transpileAndExecute( - `let concatTestTable: any[] = ${JSON.stringify(arr)}; - return JSONStringify(concatTestTable.concat(${argStr}));` - ); - - // Assert - const concatArr = arr.concat(...args); - Expect(result).toBe(JSON.stringify(concatArr)); - } - - @TestCase([], "") - @TestCase(["test1"], "test1") - @TestCase(["test1", "test2"], "test1,test2") - @TestCase(["test1", "test2"], "test1;test2", ";") - @TestCase(["test1", "test2"], "test1test2", "") - @Test("array.join") - public join(inp: T[], expected: string, seperator?: string): void { - let seperatorLua; - if (seperator === "") { - seperatorLua = "\"\""; - } else if (seperator) { - seperatorLua = "\"" + seperator + "\""; - } else { - seperatorLua = ""; - } - // Transpile/Execute - const result = util.transpileAndExecute( - `let joinTestTable = ${JSON.stringify(inp)}; - return joinTestTable.join(${seperatorLua});` - ); - - // Assert - const joinedInp = inp.join(seperator); - Expect(result).toBe(joinedInp); - } - - @TestCase([], "test1") - @TestCase(["test1"], "test1") - @TestCase(["test1", "test2"], "test2") - @TestCase(["test1", "test2", "test3"], "test3", 1) - @TestCase(["test1", "test2", "test3"], "test1", 2) - @TestCase(["test1", "test2", "test3"], "test1", -2) - @TestCase(["test1", "test2", "test3"], "test1", 12) - @Test("array.indexOf") - public indexOf(inp: string[], element: string, fromIndex?: number): void - { - let str = `return ${JSON.stringify(inp)}.indexOf("${element}");`; - if (fromIndex) { - str = `return ${JSON.stringify(inp)}.indexOf("${element}", ${fromIndex});`; - } - - // Transpile/Execute - const result = util.transpileAndExecute(str); - - // Assert - // Account for lua indexing (-1) - Expect(result).toBe(inp.indexOf(element, fromIndex)); - } - - @TestCase([1, 2, 3], 3) - @TestCase([1, 2, 3, 4, 5], 3) - @Test("array.destructuring.simple") - public arrayDestructuringSimple(inp: number[], expected: number): void - { - const result = util.transpileAndExecute( - `let [x, y, z] = ${JSON.stringify(inp)} - return z; - `); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase([1]) - @TestCase([1, 2, 3]) - @Test("array.push") - public arrayPush(inp: number[]): void - { - const result = util.transpileAndExecute( - `let testArray = [0]; - testArray.push(${inp.join(", ")}); - return JSONStringify(testArray); - ` - ); - - // Assert - Expect(result).toBe(JSON.stringify([0].concat(inp))); - } - - @TestCase("[1, 2, 3]", [3, 2]) - @TestCase("[1, 2, 3, null]", [3, 2]) - @Test("array.pop") - public arrayPop(array: string, expected): void { - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.pop(); - return val`); - - // Assert - Expect(result).toBe(expected[0]); - } - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - testArray.pop(); - return testArray.length`); - - // Assert - Expect(result).toBe(expected[1]); - } - } - - @TestCase("[1, 2, 3]", [3, 2, 1]) - @TestCase("[1, 2, 3, null]", [3, 2, 1]) - @TestCase("[1, 2, 3, 4]", [4, 3, 2, 1]) - @TestCase("[1]", [1]) - @TestCase("[]", []) - @Test("array.reverse") - public arrayReverse(array: string, expected): void - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.reverse(); - return JSONStringify(testArray)`); - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase("[1, 2, 3]", [2, 3], 1) - @TestCase("[1]", [], 1) - @TestCase("[]", [], undefined) - @Test("array.shift") - public arrayShift(array: string, expectedArray: number[], expectedValue: number): void { - { - // test array mutation - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.shift(); - return JSONStringify(testArray)`); - // Assert - Expect(result).toBe(JSON.stringify(expectedArray)); - } - // test return value - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.shift(); - return val`); - - // Assert - Expect(result).toBe(expectedValue); - } - } - } - @TestCase("[3, 4, 5]", [1, 2], [1, 2, 3, 4, 5]) - @TestCase("[]", [], []) - @TestCase("[1]", [], [1]) - @TestCase("[]", [1], [1]) - @Test("array.unshift") - public arrayUnshift(array: string, toUnshift, expected): void - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - testArray.unshift(${toUnshift}); - return JSONStringify(testArray)`); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - - @TestCase("[4, 5, 3, 2, 1]", [1, 2, 3, 4, 5]) - @TestCase("[1]", [1]) - @TestCase("[1, null]", [1]) - @TestCase("[]", []) - @Test("array.sort") - public arraySort(array: string, expected): void - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - testArray.sort(); - return JSONStringify(testArray)`); - - // Assert - Expect(result).toBe(JSON.stringify(expected)); - } - @TestCase("true", "4", "5", 4) - @TestCase("false", "4", "5", 5) - @TestCase("3", "4", "5", 4) - @Test("Ternary Conditional") - public ternaryConditional(condition: string, lhs: string, rhs: string, expected: any): void - { - const result = util.transpileAndExecute(`return ${condition} ? ${lhs} : ${rhs};`); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase("true", 11) - @TestCase("false", 13) - @TestCase("a < 4", 13) - @TestCase("a == 8", 11) - @Test("Ternary Conditional Delayed") - public ternaryConditionalDelayed(condition: string, expected: any): void - { - const result = util.transpileAndExecute( - `let a = 3; - let delay = () => ${condition} ? a + 3 : a + 5; - a = 8; - return delay();`); - - // Assert - Expect(result).toBe(expected); - } - - @TestCase("{a: 3}", "{}", {a : 3}) - @TestCase("{}", "{a: 3}", {a : 3}) - @TestCase("{a: 3}", "{a: 5}", {a : 5}) - @TestCase("{a: 3}", "{b: 5},{c: 7}", {a : 3, b: 5, c: 7}) - @Test("Object.Assign") - public objectAssign(initial: string, parameters: string, expected: object): void { - const jsonResult = util.transpileAndExecute(` - return JSONStringify(Object.assign(${initial},${parameters})); - `); - - const result = JSON.parse(jsonResult); - for (const key in expected) { - Expect(result[key]).toBe(expected[key]); - } - } - - @TestCase("{}", []) - @TestCase("{abc: 3}", ["abc,3"]) - @TestCase("{abc: 3, def: 'xyz'}", ["abc,3", "def,xyz"]) - @Test("Object.entries") - public objectEntries(obj: string, expected: string[]): void { - const result = util.transpileAndExecute(` - const obj = ${obj}; - return Object.entries(obj).map(e => e.join(",")).join(";"); - `, {target: ts.ScriptTarget.ES2018, lib: ["es2018"], luaLibImport: LuaLibImportKind.Require}) as string; - - const foundKeys = result.split(";"); - if (expected.length === 0) { - Expect(foundKeys.length).toBe(1); - Expect(foundKeys[0]).toBe(""); - } else { - Expect(foundKeys.length).toBe(expected.length); - for (const key of expected) { - Expect(foundKeys.indexOf(key) >= 0).toBeTruthy(); - } - } - } - - @TestCase("{}", []) - @TestCase("{abc: 3}", ["abc"]) - @TestCase("{abc: 3, def: 'xyz'}", ["abc", "def"]) - @Test("Object.keys") - public objectKeys(obj: string, expected: string[]): void { - const result = util.transpileAndExecute(` - const obj = ${obj}; - return Object.keys(obj).join(","); - `) as string; - - const foundKeys = result.split(","); - if (expected.length === 0) { - Expect(foundKeys.length).toBe(1); - Expect(foundKeys[0]).toBe(""); - } else { - Expect(foundKeys.length).toBe(expected.length); - for (const key of expected) { - Expect(foundKeys.indexOf(key) >= 0).toBeTruthy(); - } - } - } - - @TestCase("{}", []) - @TestCase("{abc: 'def'}", ["def"]) - @TestCase("{abc: 3, def: 'xyz'}", ["3", "xyz"]) - @Test("Object.values") - public objectValues(obj: string, expected: string[]): void { - const result = util.transpileAndExecute(` - const obj = ${obj}; - return Object.values(obj).join(","); - `, {target: ts.ScriptTarget.ES2018, lib: ["es2018"], luaLibImport: LuaLibImportKind.Require}) as string; - - const foundValues = result.split(","); - if (expected.length === 0) { - Expect(foundValues.length).toBe(1); - Expect(foundValues[0]).toBe(""); - } else { - Expect(foundValues.length).toBe(expected.length); - for (const key of expected) { - Expect(foundValues.indexOf(key) >= 0).toBeTruthy(); - } - } - } -} diff --git a/test/unit/lualib/map.spec.ts b/test/unit/lualib/map.spec.ts deleted file mode 100644 index 20e85c344..000000000 --- a/test/unit/lualib/map.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../../src/util"; - -export class MapTests { - @Test("map constructor") - public mapConstructor(): void - { - const result = util.transpileAndExecute(`let mymap = new Map(); return mymap.size;`); - - Expect(result).toBe(0); - } - - @Test("map iterable constructor") - public mapIterableConstructor(): void - { - const result = util.transpileAndExecute( - `let mymap = new Map([["a", "c"],["b", "d"]]); - return mymap.has("a") && mymap.has("b");`); - - Expect(result).toBe(true); - } - - @Test("map iterable constructor map") - public mapIterableConstructor2(): void - { - const result = util.transpileAndExecute(`let mymap = new Map(new Map([["a", "c"],["b", "d"]])); - return mymap.has("a") && mymap.has("b");`); - - Expect(result).toBe(true); - } - - @Test("map clear") - public mapClear(): void - { - const mapTS = `let mymap = new Map([["a", "c"],["b", "d"]]); mymap.clear();`; - const size = util.transpileAndExecute(mapTS + `return mymap.size;`); - Expect(size).toBe(0); - - const contains = util.transpileAndExecute(mapTS + `return !mymap.has("a") && !mymap.has("b");`); - Expect(contains).toBe(true); - } - - @Test("map delete") - public mapDelete(): void - { - const mapTS = `let mymap = new Map([["a", "c"],["b", "d"]]); mymap.delete("a");`; - const contains = util.transpileAndExecute(mapTS + `return mymap.has("b") && !mymap.has("a");`); - Expect(contains).toBe(true); - } - - @Test("map entries") - public mapEntries(): void - { - const result = util.transpileAndExecute( - `let mymap = new Map([[5, 2],[6, 3],[7, 4]]); - let count = 0; - for (var [key, value] of mymap.entries()) { count += key + value; } - return count;` - ); - Expect(result).toBe(27); - } - - @Test("map foreach") - public mapForEach(): void - { - const result = util.transpileAndExecute( - `let mymap = new Map([["a", 2],["b", 3],["c", 4]]); - let count = 0; - mymap.forEach(i => count += i); - return count;` - ); - - Expect(result).toBe(9); - } - - @Test("map foreach keys") - public mapForEachKeys(): void - { - const result = util.transpileAndExecute( - `let mymap = new Map([[5, 2],[6, 3],[7, 4]]); - let count = 0; - mymap.forEach((value, key) => { count += key; }); - return count;` - ); - - Expect(result).toBe(18); - } - - @Test("map get") - public mapGet(): void - { - const result = util.transpileAndExecute(`let mymap = new Map([["a", "c"],["b", "d"]]); return mymap.get("a");`); - - Expect(result).toBe("c"); - } - - @Test("map get missing") - public mapGetMissing(): void - { - const result = util.transpileAndExecute(`let mymap = new Map([["a", "c"],["b", "d"]]); return mymap.get("c");`); - Expect(result).toBe(undefined); - } - - @Test("map has") - public mapHas(): void - { - const contains = util.transpileAndExecute(`let mymap = new Map([["a", "c"]]); return mymap.has("a");`); - Expect(contains).toBe(true); - } - - @Test("map has false") - public mapHasFalse(): void - { - const contains = util.transpileAndExecute(`let mymap = new Map(); return mymap.has("a");`); - Expect(contains).toBe(false); - } - - @Test("map has null") - public mapHasNull(): void - { - const contains = util.transpileAndExecute(`let mymap = new Map([["a", "c"]]); return mymap.has(null);`); - Expect(contains).toBe(false); - } - - @Test("map keys") - public mapKeys(): void - { - const result = util.transpileAndExecute( - `let mymap = new Map([[5, 2],[6, 3],[7, 4]]); - let count = 0; - for (var key of mymap.keys()) { count += key; } - return count;` - ); - - Expect(result).toBe(18); - } - - @Test("map set") - public mapSet(): void - { - const mapTS = `let mymap = new Map(); mymap.set("a", 5);`; - const has = util.transpileAndExecute(mapTS + `return mymap.has("a");`); - Expect(has).toBe(true); - - const value = util.transpileAndExecute(mapTS + `return mymap.get("a")`); - Expect(value).toBe(5); - } - - @Test("map values") - public mapValues(): void { - const result = util.transpileAndExecute( - `let mymap = new Map([[5, 2],[6, 3],[7, 4]]); - let count = 0; - for (var value of mymap.values()) { count += value; } - return count;` - ); - - Expect(result).toBe(9); - } - - @Test("map size") - public mapSize(): void { - Expect(util.transpileAndExecute(`let m = new Map(); return m.size;`)).toBe(0); - Expect(util.transpileAndExecute(`let m = new Map(); m.set(1,3); return m.size;`)).toBe(1); - Expect(util.transpileAndExecute(`let m = new Map([[1,2],[3,4]]); return m.size;`)).toBe(2); - Expect(util.transpileAndExecute(`let m = new Map([[1,2],[3,4]]); m.clear(); return m.size;`)).toBe(0); - Expect(util.transpileAndExecute(`let m = new Map([[1,2],[3,4]]); m.delete(3); return m.size;`)).toBe(1); - } -} diff --git a/test/unit/lualib/set.spec.ts b/test/unit/lualib/set.spec.ts deleted file mode 100644 index 954ee06b8..000000000 --- a/test/unit/lualib/set.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../../src/util"; - -export class SetTests { - @Test("set constructor") - public setConstructor(): void - { - const result = util.transpileAndExecute(`let myset = new Set(); return myset.size;`); - - Expect(result).toBe(0); - } - - @Test("set iterable constructor") - public setIterableConstructor(): void - { - const result = util.transpileAndExecute( - `let myset = new Set(["a", "b"]); - return myset.has("a") || myset.has("b");`); - - Expect(result).toBe(true); - } - - @Test("set iterable constructor set") - public setIterableConstructorSet(): void - { - const result = util.transpileAndExecute( - `let myset = new Set(new Set(["a", "b"])); - return myset.has("a") || myset.has("b");`); - - Expect(result).toBe(true); - } - - @Test("set add") - public setAdd(): void - { - const has = util.transpileAndExecute(`let myset = new Set(); myset.add("a"); return myset.has("a");`); - Expect(has).toBe(true); - } - - @Test("set clear") - public setClear(): void - { - const setTS = `let myset = new Set(["a", "b"]); myset.clear();`; - const size = util.transpileAndExecute(setTS + `return myset.size;`); - Expect(size).toBe(0); - - const contains = util.transpileAndExecute(setTS + `return !myset.has("a") && !myset.has("b");`); - Expect(contains).toBe(true); - } - - @Test("set delete") - public setDelete(): void - { - const setTS = `let myset = new Set(["a", "b"]); myset.delete("a");`; - const contains = util.transpileAndExecute(setTS + `return myset.has("b") && !myset.has("a");`); - Expect(contains).toBe(true); - } - - @Test("set entries") - public setEntries(): void - { - const result = util.transpileAndExecute( - `let myset = new Set([5, 6, 7]); - let count = 0; - for (var [key, value] of myset.entries()) { count += key + value; } - return count;` - ); - - Expect(result).toBe(36); - } - - @Test("set foreach") - public setForEach(): void - { - const result = util.transpileAndExecute( - `let myset = new Set([2, 3, 4]); - let count = 0; - myset.forEach(i => { count += i; }); - return count;` - ); - Expect(result).toBe(9); - } - - @Test("set foreach keys") - public setForEachKeys(): void - { - const result = util.transpileAndExecute( - `let myset = new Set([2, 3, 4]); - let count = 0; - myset.forEach((value, key) => { count += key; }); - return count;` - ); - - Expect(result).toBe(9); - } - - @Test("set has") - public setHas(): void - { - const contains = util.transpileAndExecute(`let myset = new Set(["a", "c"]); return myset.has("a");`); - Expect(contains).toBe(true); - } - - @Test("set has false") - public setHasFalse(): void - { - const contains = util.transpileAndExecute(`let myset = new Set(); return myset.has("a");`); - Expect(contains).toBe(false); - } - - @Test("set has null") - public setHasNull(): void - { - const contains = util.transpileAndExecute(`let myset = new Set(["a", "c"]); return myset.has(null);`); - Expect(contains).toBe(false); - } - - @Test("set keys") - public setKeys(): void - { - const result = util.transpileAndExecute( - `let myset = new Set([5, 6, 7]); - let count = 0; - for (var key of myset.keys()) { count += key; } - return count;` - ); - - Expect(result).toBe(18); - } - - @Test("set values") - public setValues(): void - { - const result = util.transpileAndExecute( - `let myset = new Set([5, 6, 7]); - let count = 0; - for (var value of myset.values()) { count += value; } - return count;` - ); - - Expect(result).toBe(18); - } - - @Test("set size") - public setSize(): void { - Expect(util.transpileAndExecute(`let m = new Set(); return m.size;`)).toBe(0); - Expect(util.transpileAndExecute(`let m = new Set(); m.add(1); return m.size;`)).toBe(1); - Expect(util.transpileAndExecute(`let m = new Set([1, 2]); return m.size;`)).toBe(2); - Expect(util.transpileAndExecute(`let m = new Set([1, 2]); m.clear(); return m.size;`)).toBe(0); - Expect(util.transpileAndExecute(`let m = new Set([1, 2]); m.delete(2); return m.size;`)).toBe(1); - } -} diff --git a/test/unit/lualib/symbol.spec.ts b/test/unit/lualib/symbol.spec.ts deleted file mode 100644 index a15b79664..000000000 --- a/test/unit/lualib/symbol.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../../src/util"; -import { CompilerOptions, LuaLibImportKind } from '../../../src/CompilerOptions'; - -export class SymbolTests { - private compilerOptions: CompilerOptions = { - lib: ["esnext"], - luaLibImport: LuaLibImportKind.Require, - }; - - @Test("symbol.toString()") - @TestCase() - @TestCase(1) - @TestCase("name") - public symbolToString(description?: string | number): void - { - const result = util.transpileAndExecute(` - return Symbol(${JSON.stringify(description)}).toString(); - `, this.compilerOptions); - - Expect(result).toBe(`Symbol(${description || ''})`); - } - - @Test("symbol.description") - @TestCase() - @TestCase(1) - @TestCase("name") - public symbolDescription(description?: string | number): void - { - const result = util.transpileAndExecute(` - return Symbol(${JSON.stringify(description)}).description; - `, this.compilerOptions); - - Expect(result).toBe(description); - } - - @Test("symbol uniqueness") - public symbolUniqueness(): void - { - const result = util.transpileAndExecute(` - return Symbol("a") === Symbol("a"); - `); - - Expect(result).toBe(false); - } - - @Test("Symbol.for") - public symbolFor(): void - { - const result = util.transpileAndExecute(` - return Symbol.for("name").description; - `, this.compilerOptions); - - Expect(result).toBe("name"); - } - - @Test("Symbol.for non-uniqueness") - public symbolForNonUniqueness(): void - { - const result = util.transpileAndExecute(` - return Symbol.for("a") === Symbol.for("a"); - `); - - Expect(result).toBe(true); - } - - @Test("Symbol.keyFor") - public symbolKeyFor(): void - { - const result = util.transpileAndExecute(` - const sym = Symbol.for("a"); - Symbol.for("b"); - return Symbol.keyFor(sym); - `); - - Expect(result).toBe("a"); - } - - @Test("Symbol.keyFor empty") - public symbolKeyForEmpty(): void - { - const result = util.transpileAndExecute(` - Symbol.for("a"); - return Symbol.keyFor(Symbol()); - `); - - Expect(result).toBe(undefined); - } -} diff --git a/test/unit/lualib/weakMap.spec.ts b/test/unit/lualib/weakMap.spec.ts deleted file mode 100644 index d9e829778..000000000 --- a/test/unit/lualib/weakMap.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Expect, Test } from "alsatian"; -import * as util from "../../src/util"; - -export class WeakMapTests { - private initRefsTs = `let ref = {}; - let ref2 = () => {};`; - - @Test("weakMap constructor") - public weakMapConstructor(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[ref, 1]]); - return mymap.get(ref); - `); - - Expect(result).toBe(1); - } - - @Test("weakMap iterable constructor") - public weakMapIterableConstructor(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[ref, 1], [ref2, 2]]); - return mymap.has(ref) && mymap.has(ref2); - `); - - Expect(result).toBe(true); - } - - @Test("weakMap iterable constructor map") - public weakMapIterableConstructor2(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap(new Map([[ref, 1], [ref2, 2]])); - return mymap.has(ref) && mymap.has(ref2); - `); - - Expect(result).toBe(true); - } - - @Test("weakMap delete") - public weakMapDelete(): void - { - const contains = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[ref, true], [ref2, true]]); - mymap.delete(ref2); - return mymap.has(ref) && !mymap.has(ref2); - `); - - Expect(contains).toBe(true); - } - - @Test("weakMap get") - public weakMapGet(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[ref, 1], [{}, 2]]); - return mymap.get(ref); - `); - - Expect(result).toBe(1); - } - - @Test("weakMap get missing") - public weakMapGetMissing(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[{}, true]]); - return mymap.get({}); - `); - - Expect(result).toBe(undefined); - } - - @Test("weakMap has") - public weakMapHas(): void - { - const contains = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[ref, true]]); - return mymap.has(ref); - `); - - Expect(contains).toBe(true); - } - - @Test("weakMap has false") - public weakMapHasFalse(): void - { - const contains = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[ref, true]]); - return mymap.has(ref2); - `); - - Expect(contains).toBe(false); - } - - @Test("weakMap has null") - public weakMapHasNull(): void - { - const contains = util.transpileAndExecute(this.initRefsTs + ` - let mymap = new WeakMap([[{}, true]]); - return mymap.has(null); - `); - - Expect(contains).toBe(false); - } - - @Test("weakMap set") - public weakMapSet(): void - { - const init = this.initRefsTs + ` - let mymap = new WeakMap(); - mymap.set(ref, 5); - `; - - const has = util.transpileAndExecute(init + `return mymap.has(ref);`); - Expect(has).toBe(true); - - const value = util.transpileAndExecute(init + `return mymap.get(ref)`); - Expect(value).toBe(5); - } - - @Test("weakMap has no map features") - public weakMapHasNoMapFeatures(): void - { - const transpileAndExecute = (tsStr: string) => util.transpileAndExecute(tsStr, undefined, undefined, undefined, true); - Expect(transpileAndExecute(`return new WeakMap().size`)).toBe(undefined); - Expect(() => transpileAndExecute(`new WeakMap().clear()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakMap().keys()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakMap().values()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakMap().entries()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakMap().forEach(() => {})`)).toThrow(); - } -} diff --git a/test/unit/lualib/weakSet.spec.ts b/test/unit/lualib/weakSet.spec.ts deleted file mode 100644 index e4425618e..000000000 --- a/test/unit/lualib/weakSet.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Expect, Test } from "alsatian"; -import * as util from "../../src/util"; - -export class WeakSetTests { - private initRefsTs = `let ref = {}; - let ref2 = () => {};`; - - @Test("weakSet constructor") - public weakSetConstructor(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let myset = new WeakSet([ref]); - return myset.has(ref) - `); - - Expect(result).toBe(true); - } - - @Test("weakSet iterable constructor") - public weakSetIterableConstructor(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let myset = new WeakSet([ref, ref2]); - return myset.has(ref) && myset.has(ref2); - `); - - Expect(result).toBe(true); - } - - @Test("weakSet iterable constructor set") - public weakSetIterableConstructorSet(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let myset = new WeakSet(new Set([ref, ref2])); - return myset.has(ref) && myset.has(ref2); - `); - - Expect(result).toBe(true); - } - - @Test("weakSet add") - public weakSetAdd(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let myset = new WeakSet(); - myset.add(ref); - return myset.has(ref); - `); - - Expect(result).toBe(true); - } - - @Test("weakSet add different references") - public weakSetAddDifferentReferences(): void - { - const result = util.transpileAndExecute(this.initRefsTs + ` - let myset = new WeakSet(); - myset.add({}); - return myset.has({}); - `); - - Expect(result).toBe(false); - } - - @Test("weakSet delete") - public weakSetDelete(): void - { - const contains = util.transpileAndExecute(this.initRefsTs + ` - let myset = new WeakSet([ref, ref2]); - myset.delete(ref); - return myset.has(ref2) && !myset.has(ref); - `); - Expect(contains).toBe(true); - } - - @Test("weakSet has no set features") - public weakSetHasNoSetFeatures(): void - { - const transpileAndExecute = (tsStr: string) => util.transpileAndExecute(tsStr, undefined, undefined, undefined, true); - Expect(transpileAndExecute(`return new WeakSet().size`)).toBe(undefined); - Expect(() => transpileAndExecute(`new WeakSet().clear()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakSet().keys()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakSet().values()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakSet().entries()`)).toThrow(); - Expect(() => transpileAndExecute(`new WeakSet().forEach(() => {})`)).toThrow(); - } -} diff --git a/test/unit/math.spec.ts b/test/unit/math.spec.ts deleted file mode 100644 index ad7a37169..000000000 --- a/test/unit/math.spec.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { Expect, Test, TestCase, IgnoreTest } from "alsatian"; -import * as util from "../src/util"; - -export class MathTests { - - // Dont test all and dont do functional test - // because math implementations may differ between js and lua - @TestCase("Math.cos()", "math.cos();") - @TestCase("Math.sin()", "math.sin();") - @TestCase("Math.min()", "math.min();") - @TestCase("Math.PI", "math.pi;") - @Test("Math") - public math(inp: string, expected: string): void { - // Transpile - const lua = util.transpileString(inp); - - // Assert - Expect(lua).toBe(expected); - } - - @TestCase("++x", "x=4;y=6") - @TestCase("x++", "x=4;y=6") - @TestCase("--x", "x=2;y=6") - @TestCase("x--", "x=2;y=6") - @TestCase("x += y", "x=9;y=6") - @TestCase("x -= y", "x=-3;y=6") - @TestCase("x *= y", "x=18;y=6") - @TestCase("y /= x", "x=3;y=2.0") - @TestCase("y %= x", "x=3;y=0") - @TestCase("y **= x", "x=3;y=216.0") - @TestCase("x |= y", "x=7;y=6") - @TestCase("x &= y", "x=2;y=6") - @TestCase("x ^= y", "x=5;y=6") - @TestCase("x <<= y", "x=192;y=6") - @TestCase("x >>= y", "x=0;y=6") - @Test("Operator assignment statements") - public opAssignmentStatement(statement: string, expected: string): void { - const result = util.transpileAndExecute( - `let x = 3; - let y = 6; - ${statement}; - return \`x=\${x};y=\${y}\``); - Expect(result).toBe(expected); - } - - @TestCase("++o.p", "o=4;a=6") - @TestCase("o.p++", "o=4;a=6") - @TestCase("--o.p", "o=2;a=6") - @TestCase("o.p--", "o=2;a=6") - @TestCase("o.p += a[0]", "o=9;a=6") - @TestCase("o.p -= a[0]", "o=-3;a=6") - @TestCase("o.p *= a[0]", "o=18;a=6") - @TestCase("a[0] /= o.p", "o=3;a=2.0") - @TestCase("a[0] %= o.p", "o=3;a=0") - @TestCase("a[0] **= o.p", "o=3;a=216.0") - @TestCase("o.p |= a[0]", "o=7;a=6") - @TestCase("o.p &= a[0]", "o=2;a=6") - @TestCase("o.p ^= a[0]", "o=5;a=6") - @TestCase("o.p <<= a[0]", "o=192;a=6") - @TestCase("o.p >>= a[0]", "o=0;a=6") - @Test("Operator assignment to simple property statements") - public opSimplePropAssignmentStatement(statement: string, expected: string): void { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - ${statement}; - return \`o=\${o.p};a=\${a[0]}\``); - Expect(result).toBe(expected); - } - - @TestCase("++o.p.d", "o=4;a=[6,11],[7,13]") - @TestCase("o.p.d++", "o=4;a=[6,11],[7,13]") - @TestCase("--o.p.d", "o=2;a=[6,11],[7,13]") - @TestCase("o.p.d--", "o=2;a=[6,11],[7,13]") - @TestCase("o.p.d += a[0][0]", "o=9;a=[6,11],[7,13]") - @TestCase("o.p.d -= a[0][0]", "o=-3;a=[6,11],[7,13]") - @TestCase("o.p.d *= a[0][0]", "o=18;a=[6,11],[7,13]") - @TestCase("a[0][0] /= o.p.d", "o=3;a=[2.0,11],[7,13]") - @TestCase("a[0][0] %= o.p.d", "o=3;a=[0,11],[7,13]") - @TestCase("a[0][0] **= o.p.d", "o=3;a=[216.0,11],[7,13]") - @TestCase("o.p.d |= a[0][0]", "o=7;a=[6,11],[7,13]") - @TestCase("o.p.d &= a[0][0]", "o=2;a=[6,11],[7,13]") - @TestCase("o.p.d ^= a[0][0]", "o=5;a=[6,11],[7,13]") - @TestCase("o.p.d <<= a[0][0]", "o=192;a=[6,11],[7,13]") - @TestCase("o.p.d >>= a[0][0]", "o=0;a=[6,11],[7,13]") - @Test("Operator assignment to deep property statements") - public opDeepPropAssignmentStatement(statement: string, expected: string): void { - const result = util.transpileAndExecute( - `let o = {p: {d: 3}}; - let a = [[6,11], [7,13]]; - ${statement}; - return \`o=\${o.p.d};a=[\${a[0][0]},\${a[0][1]}],[\${a[1][0]},\${a[1][1]}]\``); - Expect(result).toBe(expected); - } - - @TestCase("++of().p", "o=4;a=6") - @TestCase("of().p++", "o=4;a=6") - @TestCase("--of().p", "o=2;a=6") - @TestCase("of().p--", "o=2;a=6") - @TestCase("of().p += af()[i()]", "o=9;a=6") - @TestCase("of().p -= af()[i()]", "o=-3;a=6") - @TestCase("of().p *= af()[i()]", "o=18;a=6") - @TestCase("af()[i()] /= of().p", "o=3;a=2.0") - @TestCase("af()[i()] %= of().p", "o=3;a=0") - @TestCase("af()[i()] **= of().p", "o=3;a=216.0") - @TestCase("of().p |= af()[i()]", "o=7;a=6") - @TestCase("of().p &= af()[i()]", "o=2;a=6") - @TestCase("of().p ^= af()[i()]", "o=5;a=6") - @TestCase("of().p <<= af()[i()]", "o=192;a=6") - @TestCase("of().p >>= af()[i()]", "o=0;a=6") - @Test("Operator assignment to complex property statements") - public opComplexPropAssignmentStatement(statement: string, expected: string): void { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - function of() { return o; } - function af() { return a; } - function i() { return 0; } - ${statement}; - return \`o=\${o.p};a=\${a[0]}\``); - Expect(result).toBe(expected); - } - - @TestCase("++of().p.d", "o=4;a=[7,6],[11,13];i=0") - @TestCase("of().p.d++", "o=4;a=[7,6],[11,13];i=0") - @TestCase("--of().p.d", "o=2;a=[7,6],[11,13];i=0") - @TestCase("of().p.d--", "o=2;a=[7,6],[11,13];i=0") - @TestCase("of().p.d += af()[i()][i()]", "o=9;a=[7,6],[11,13];i=2") - @TestCase("of().p.d -= af()[i()][i()]", "o=-3;a=[7,6],[11,13];i=2") - @TestCase("of().p.d *= af()[i()][i()]", "o=18;a=[7,6],[11,13];i=2") - @TestCase("af()[i()][i()] /= of().p.d", "o=3;a=[7,2.0],[11,13];i=2") - @TestCase("af()[i()][i()] %= of().p.d", "o=3;a=[7,0],[11,13];i=2") - @TestCase("af()[i()][i()] **= of().p.d", "o=3;a=[7,216.0],[11,13];i=2") - @TestCase("of().p.d |= af()[i()][i()]", "o=7;a=[7,6],[11,13];i=2") - @TestCase("of().p.d &= af()[i()][i()]", "o=2;a=[7,6],[11,13];i=2") - @TestCase("of().p.d ^= af()[i()][i()]", "o=5;a=[7,6],[11,13];i=2") - @TestCase("of().p.d <<= af()[i()][i()]", "o=192;a=[7,6],[11,13];i=2") - @TestCase("of().p.d >>= af()[i()][i()]", "o=0;a=[7,6],[11,13];i=2") - @Test("Operator assignment to complex deep property statements") - public opComplexDeepPropAssignmentStatement(statement: string, expected: string): void { - const result = util.transpileAndExecute( - `let o = {p: {d: 3}}; - let a = [[7, 6], [11, 13]]; - function of() { return o; } - function af() { return a; } - let _i = 0; - function i() { return _i++; } - ${statement}; - return \`o=\${o.p.d};a=[\${a[0][0]},\${a[0][1]}],[\${a[1][0]},\${a[1][1]}];i=\${_i}\``); - Expect(result).toBe(expected); - } - - @TestCase("++x", "4;x=4;y=6") - @TestCase("x++", "3;x=4;y=6") - @TestCase("--x", "2;x=2;y=6") - @TestCase("x--", "3;x=2;y=6") - @TestCase("x += y", "9;x=9;y=6") - @TestCase("x -= y", "-3;x=-3;y=6") - @TestCase("x *= y", "18;x=18;y=6") - @TestCase("y /= x", "2.0;x=3;y=2.0") - @TestCase("y %= x", "0;x=3;y=0") - @TestCase("y **= x", "216.0;x=3;y=216.0") - @TestCase("x |= y", "7;x=7;y=6") - @TestCase("x &= y", "2;x=2;y=6") - @TestCase("x ^= y", "5;x=5;y=6") - @TestCase("x <<= y", "192;x=192;y=6") - @TestCase("x >>= y", "0;x=0;y=6") - @TestCase("x + (y += 7)", "16;x=3;y=13") - @TestCase("x + (y += 7)", "16;x=3;y=13") - @TestCase("x++ + (y += 7)", "16;x=4;y=13") - @Test("Operator assignment expressions") - public opAssignmentExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute( - `let x = 3; - let y = 6; - const r = ${expression}; - return \`\${r};x=\${x};y=\${y}\``); - Expect(result).toBe(expected); - } - - @TestCase("++o.p", "4;o=4;a=6") - @TestCase("o.p++", "3;o=4;a=6") - @TestCase("--o.p", "2;o=2;a=6") - @TestCase("o.p--", "3;o=2;a=6") - @TestCase("o.p += a[0]", "9;o=9;a=6") - @TestCase("o.p -= a[0]", "-3;o=-3;a=6") - @TestCase("o.p *= a[0]", "18;o=18;a=6") - @TestCase("a[0] /= o.p", "2.0;o=3;a=2.0") - @TestCase("a[0] %= o.p", "0;o=3;a=0") - @TestCase("a[0] **= o.p", "216.0;o=3;a=216.0") - @TestCase("o.p |= a[0]", "7;o=7;a=6") - @TestCase("o.p &= a[0]", "2;o=2;a=6") - @TestCase("o.p ^= a[0]", "5;o=5;a=6") - @TestCase("o.p <<= a[0]", "192;o=192;a=6") - @TestCase("o.p >>= a[0]", "0;o=0;a=6") - @TestCase("o.p + (a[0] += 7)", "16;o=3;a=13") - @TestCase("o.p += (a[0] += 7)", "16;o=16;a=13") - @TestCase("o.p++ + (a[0] += 7)", "16;o=4;a=13") - @Test("Operator assignment to simple property expressions") - public opSimplePropAssignmentExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - const r = ${expression}; - return \`\${r};o=\${o.p};a=\${a[0]}\``); - Expect(result).toBe(expected); - } - - @TestCase("++of().p", "4;o=4;a=6") - @TestCase("of().p++", "3;o=4;a=6") - @TestCase("--of().p", "2;o=2;a=6") - @TestCase("of().p--", "3;o=2;a=6") - @TestCase("of().p += af()[i()]", "9;o=9;a=6") - @TestCase("of().p -= af()[i()]", "-3;o=-3;a=6") - @TestCase("of().p *= af()[i()]", "18;o=18;a=6") - @TestCase("af()[i()] /= of().p", "2.0;o=3;a=2.0") - @TestCase("af()[i()] %= of().p", "0;o=3;a=0") - @TestCase("af()[i()] **= of().p", "216.0;o=3;a=216.0") - @TestCase("of().p |= af()[i()]", "7;o=7;a=6") - @TestCase("of().p &= af()[i()]", "2;o=2;a=6") - @TestCase("of().p ^= af()[i()]", "5;o=5;a=6") - @TestCase("of().p <<= af()[i()]", "192;o=192;a=6") - @TestCase("of().p >>= af()[i()]", "0;o=0;a=6") - @TestCase("of().p + (af()[i()] += 7)", "16;o=3;a=13") - @TestCase("of().p += (af()[i()] += 7)", "16;o=16;a=13") - @TestCase("of().p++ + (af()[i()] += 7)", "16;o=4;a=13") - @Test("Operator assignment to complex property expressions") - public opComplexPropAssignmentExpression(expression: string, expected: string): void { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - function of() { return o; } - function af() { return a; } - function i() { return 0; } - const r = ${expression}; - return \`\${r};o=\${o.p};a=\${a[0]}\``); - Expect(result).toBe(expected); - } -} diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts deleted file mode 100644 index f5a6ad416..000000000 --- a/test/unit/modules.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; - -import * as ts from "typescript"; -import * as util from "../src/util"; - -export class LuaModuleTests { - - @Test("defaultImport") - public defaultImport(): void { - Expect(() => { - const lua = util.transpileString(`import TestClass from "test"`); - }).toThrowError(Error, "Default Imports are not supported, please use named imports instead!"); - } - - @Test("lualibRequire") - public lualibRequire(): void { - // Transpile - const lua = util.transpileString(`let a = b instanceof c;`, - { luaLibImport: LuaLibImportKind.Require, luaTarget: LuaTarget.LuaJIT }); - - // Assert - Expect(lua.startsWith(`require("lualib_bundle")`)); - } - - @Test("lualibRequireAlways") - public lualibRequireAlways(): void { - // Transpile - const lua = util.transpileString(``, { luaLibImport: LuaLibImportKind.Always, luaTarget: LuaTarget.LuaJIT }); - - // Assert - Expect(lua).toBe(`require("lualib_bundle");`); - } - - @Test("Non-exported module") - public nonExportedModule(): void { - const result = util.transpileAndExecute( - "return g.test();", - undefined, - undefined, - "module g { export function test() { return 3; } }" // Typescript header - ); - - Expect(result).toBe(3); - } - - @TestCase(LuaLibImportKind.Inline) - @TestCase(LuaLibImportKind.None) - @TestCase(LuaLibImportKind.Require) - @Test("LuaLib no uses? No code") - public lualibNoUsesNoCode(impKind: LuaLibImportKind): void { - // Transpile - const lua = util.transpileString(``, { luaLibImport: impKind }); - - // Assert - Expect(lua).toBe(``); - } - - @Test("Nested module with dot in name") - public nestedModuleWithDotInName(): void { - const code = - `module a.b { - export const foo = "foo"; - }`; - Expect(util.transpileAndExecute("return a.b.foo;", undefined, undefined, code)).toBe("foo"); - } -} diff --git a/test/unit/modules/__snapshots__/resolution.spec.ts.snap b/test/unit/modules/__snapshots__/resolution.spec.ts.snap new file mode 100644 index 000000000..8c40101e5 --- /dev/null +++ b/test/unit/modules/__snapshots__/resolution.spec.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`doesn't resolve paths out of root dir: code 1`] = ` +"local ____exports = {} +local module = require("module") +local ____ = module +return ____exports" +`; + +exports[`doesn't resolve paths out of root dir: diagnostics 1`] = `"error TSTL: Could not resolve lua source files for require path '../module' in file main.ts."`; diff --git a/test/unit/modules/modules.spec.ts b/test/unit/modules/modules.spec.ts new file mode 100644 index 000000000..d15f18463 --- /dev/null +++ b/test/unit/modules/modules.spec.ts @@ -0,0 +1,315 @@ +import * as ts from "typescript"; +import * as util from "../../util"; + +describe("module import/export elision", () => { + const moduleDeclaration = ` + declare module "module" { + export type Type = string; + export declare const value: string; + } + `; + + const expectToElideImport: util.TapCallback = builder => { + builder.addExtraFile("module.d.ts", moduleDeclaration).setOptions({ module: ts.ModuleKind.CommonJS }); + expect(builder.getLuaExecutionResult()).not.toBeInstanceOf(util.ExecutionError); + }; + + test("should elide named type imports", () => { + util.testModule` + import { Type } from "module"; + const foo: Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide named value imports used only as a type", () => { + util.testModule` + import { value } from "module"; + const foo: typeof value = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide namespace imports with unused values", () => { + util.testModule` + import * as module from "module"; + const foo: module.Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide `import =` declarations", () => { + util.testModule` + import module = require("module"); + const foo: module.Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide type exports", () => { + util.testModule` + (globalThis as any).foo = true; + type foo = boolean; + export { foo }; + `.expectToEqual([]); + }); +}); + +test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])( + "Import module names with invalid lua identifier characters (%p)", + name => { + util.testModule` + import { foo } from "./${name}"; + export { foo }; + ` + .addExtraFile(`${name}.ts`, 'export const foo = "bar";') + .setReturnExport("foo") + .expectToEqual("bar"); + } +); + +test.each(["export default value;", "export { value as default };"])("Export Default From (%p)", exportStatement => { + util.testModule` + export { default } from "./module"; + ` + .addExtraFile( + "module.ts", + ` + export const value = true; + ${exportStatement}; + ` + ) + .expectToMatchJsResult(); +}); + +test("Default Import and Export Expression", () => { + util.testModule` + import defaultExport from "./module"; + export const value = defaultExport; + ` + .addExtraFile( + "module.ts", + ` + export default 1 + 2 + 3; + ` + ) + .expectToMatchJsResult(); +}); + +test("Import and Export Assignment", () => { + util.testModule` + // @ts-ignore + import * as m from "./module"; + export const value = m; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .addExtraFile( + "module.ts", + ` + export = true; + ` + ) + .expectToMatchJsResult(); +}); + +test("Mixed Exports, Default and Named Imports", () => { + util.testModule` + import defaultExport, { a, b, c } from "./module"; + export const value = defaultExport + b + c; + ` + .addExtraFile( + "module.ts", + ` + export const a = 1; + export const b = 2; + export const c = 3; + export default a; + ` + ) + .expectToMatchJsResult(); +}); + +test("Mixed Exports, Default and Namespace Import", () => { + util.testModule` + import defaultExport, * as ns from "./module"; + export const value = defaultExport + ns.b + ns.c; + ` + .addExtraFile( + "module.ts", + ` + export const a = 1; + export const b = 2; + export const c = 3; + export default a; + ` + ) + .expectToMatchJsResult(); +}); + +test("Export Default Function", () => { + const mainCode = ` + import defaultExport from "./module"; + export const value = defaultExport(); + `; + util.testModule(mainCode) + .addExtraFile( + "module.ts", + ` + export default function() { + return true; + } + ` + ) + .expectToMatchJsResult(); +}); + +test("Export Equals", () => { + util.testModule` + export = true; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .expectToMatchJsResult(); +}); + +const reassignmentTestCases = [ + "x = 1", + "x++", + "(x = 1)", + "[x] = [1]", + "[[x]] = [[1]]", + "({ x } = { x: 1 })", + "({ y: x } = { y: 1 })", + "({ x = 1 } = { x: undefined })", +]; + +test.each(reassignmentTestCases)("export specifier with reassignment afterwards (%p)", reassignment => { + util.testModule` + let x = 0; + export { x }; + ${reassignment}; + `.expectToMatchJsResult(); +}); + +test.each(reassignmentTestCases)("export specifier fork (%p)", reassignment => { + util.testModule` + let x = 0; + export { x as a }; + export { x as b }; + ${reassignment}; + `.expectToMatchJsResult(); +}); + +test("does not export shadowed identifiers", () => { + util.testModule` + export let a = 1; + { let a = 2; a = 3 }; + `.expectToMatchJsResult(); +}); + +test("export as specifier shouldn't effect local vars", () => { + util.testModule` + let x = false; + export { x as a }; + let a = 5; + a = 6; + `.expectToMatchJsResult(); +}); + +test("export modified in for in loop", () => { + util.testModule` + export let foo = ''; + for (foo in { x: true }) {} + ` + .setReturnExport("foo") + .expectToMatchJsResult(); +}); + +test("export dependency modified in for in loop", () => { + util.testModule` + let foo = ''; + export { foo as bar }; + for (foo in { x: true }) {} + ` + .setReturnExport("bar") + .expectToEqual("x"); +}); + +test("export default class with future reference", () => { + util.testModule` + export default class Default {} + const d = new Default(); + export const result = d.constructor.name; + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("export default function with future reference", () => { + util.testModule` + export default function defaultFunction() { + return true; + } + export const result = defaultFunction(); + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +const moduleFile = ` +export default true; +export const foo = "bar"; +`; + +test("export all does not include default", () => { + util.testModule` + export * from "./module"; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .addExtraFile("module.ts", moduleFile) + .expectToMatchJsResult(); +}); + +test("namespace export does not include default", () => { + util.testModule` + export * as result from "./module"; + ` + .addExtraFile("module.ts", moduleFile) + .expectToMatchJsResult(); +}); + +test("namespace export with unsafe Lua name", () => { + util.testModule` + export * as $$$ from "./module"; + ` + .addExtraFile("module.ts", moduleFile) + .expectToMatchJsResult(); +}); + +test("import expression", () => { + util.testModule` + let result; + import("./module").then(m => { result = m.foo(); }); + export { result }; + ` + .addExtraFile("module.ts", 'export function foo() { return "foo"; }') + .setOptions({ module: ts.ModuleKind.ESNext }) + .expectToEqual({ result: "foo" }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1572 +test("correctly exports @compileMembersOnly enums (#1572)", () => { + util.testModule` + export { val } from "./otherfile"; + ` + .addExtraFile( + "otherfile.ts", + ` + // Would put this in the main file, but we cannot transfer enum types over lua/js boundary + // but we still need to have an exported enum, hence it is in another file + /** @compileMembersOnly */ + export enum MyEnum { + A = 0, + B = 1, + C = 2 + } + export const val = MyEnum.B | MyEnum.C; + ` + ) + .expectToEqual({ val: 3 }); +}); diff --git a/test/unit/modules/resolution.spec.ts b/test/unit/modules/resolution.spec.ts new file mode 100644 index 000000000..26c147c68 --- /dev/null +++ b/test/unit/modules/resolution.spec.ts @@ -0,0 +1,176 @@ +import * as ts from "typescript"; +import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; +import * as util from "../../util"; + +const requireRegex = /require\("(.*?)"\)/; +const expectToRequire = + (expected: string): util.TapCallback => + builder => { + const [, requiredPath] = builder.getMainLuaCodeChunk().match(requireRegex) ?? []; + expect(requiredPath).toBe(expected); + }; + +test.each([ + { + filePath: "main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "." }, + }, + { + filePath: "main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "./" }, + }, + { + filePath: "src/main.ts", + usedPath: "./folder/Module", + expected: "src.folder.Module", + options: { rootDir: "." }, + }, + { + filePath: "main.ts", + usedPath: "folder/Module", + expected: "folder.Module", + options: { rootDir: ".", baseUrl: "." }, + }, + { + filePath: "main.ts", + usedPath: "folder/Module", + expected: "folder.Module", + options: { rootDir: "./", baseUrl: "." }, + }, + { + filePath: "src/main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "src" }, + }, + { + filePath: "src/main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "./src" }, + }, + { + filePath: "src/dir/main.ts", + usedPath: "../Module", + expected: "Module", + options: { rootDir: "./src" }, + }, + { + filePath: "src/dir/dir/main.ts", + usedPath: "../../dir/Module", + expected: "dir.Module", + options: { rootDir: "./src" }, + }, +])("resolve paths with baseUrl or rootDir (%p)", ({ filePath, usedPath, expected, options }) => { + util.testModule` + import * as module from "${usedPath}"; + module; + ` + .setMainFileName(filePath) + .addExtraFile(`${usedPath}.ts`, "") + .setOptions(options) + .tap(expectToRequire(expected)); +}); + +test("doesn't resolve paths out of root dir", () => { + util.testModule` + import * as module from "../module"; + module; + ` + .setMainFileName("src/main.ts") + .setOptions({ rootDir: "./src" }) + .disableSemanticCheck() + .expectDiagnosticsToMatchSnapshot([couldNotResolveRequire.code]); +}); + +test("resolves non-standard requires", () => { + const { transpiledFiles } = util.testModule` + export * from "./externalLua"; + ` + .addExtraFile("externalLua.d.ts", "export const foo = 3;") + .addExtraFile( + "externalLua.lua", + ` + require("requiredLuaFile1") -- standard + require('requiredLuaFile2') -- single quote + require'requiredLuaFile3' -- no parentheses + require"requiredLuaFile4" -- no parentheses double quote + require "requiredLuaFile5" -- no parentheses and space + require "requiredLua'File6" -- no parentheses and space + require 'requiredLua"File7' -- no parentheses and space + ` + ) + .addExtraFile("requiredLuaFile1.lua", "") + .addExtraFile("requiredLuaFile2.lua", "") + .addExtraFile("requiredLuaFile3.lua", "") + .addExtraFile("requiredLuaFile4.lua", "") + .addExtraFile("requiredLuaFile5.lua", "") + .addExtraFile("requiredLua'File6.lua", "") + .addExtraFile('requiredLua"File7.lua', "") + .expectToHaveNoDiagnostics() + .getLuaResult(); + + // Expect main.lua, externalLua.lua and all 7 required lua files in there + expect(transpiledFiles.map(f => f.outPath)).toHaveLength(9); +}); + +test.each([ + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" {} + `, + mainCode: 'import "fake";', + expectedPath: "fake", + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" {} + `, + mainCode: 'import * as fake from "fake"; fake;', + expectedPath: "fake", + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" { + export const x: number; + } + `, + mainCode: 'import { x } from "fake"; x;', + expectedPath: "fake", + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" { + export const x: number; + } + + declare module "fake" { + export const y: number; + } + `, + mainCode: 'import { y } from "fake"; y;', + expectedPath: "fake", + }, +])("noResolution prevents any module path resolution behavior", ({ declarationStatement, mainCode, expectedPath }) => { + util.testModule(mainCode) + .setMainFileName("src/main.ts") + .addExtraFile("module.d.ts", declarationStatement) + .tap(expectToRequire(expectedPath)); +}); + +test("import = require", () => { + util.testModule` + import foo = require("./foo/bar"); + foo; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .tap(expectToRequire("foo.bar")); +}); diff --git a/test/unit/namespaces.spec.ts b/test/unit/namespaces.spec.ts new file mode 100644 index 000000000..01b2e3f92 --- /dev/null +++ b/test/unit/namespaces.spec.ts @@ -0,0 +1,158 @@ +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"; } }') + .expectToMatchJsResult(); +}); + +test("nested namespace", () => { + util.testModule` + namespace A { + export namespace B { + export const foo = "foo"; + } + } + + export const foo = A.B.foo; + `.expectToMatchJsResult(); +}); + +test("nested namespace with dot in name", () => { + util.testModule` + namespace A.B { + export const foo = "foo"; + } + + export const foo = A.B.foo; + `.expectToMatchJsResult(); +}); + +test("context in namespace function", () => { + util.testModule` + namespace a { + export const foo = "foo"; + export function bar() { return this.foo + "bar"; } + } + + export const result = a.bar(); + `.expectToMatchJsResult(); +}); + +test("namespace merging", () => { + util.testModule` + export namespace Foo { + export const a = 1; + } + + export namespace Foo { + export const b = 2; + } + `.expectToMatchJsResult(); +}); + +test("namespace merging with interface", () => { + util.testModule` + interface Foo {} + namespace Foo { + export function bar() { return "foobar"; } + } + + export const result = Foo.bar(); + `.expectToMatchJsResult(); +}); + +test("namespace merging across files", () => { + const a = ` + namespace NS { + export namespace Inner { + export const foo = "foo"; + } + } + `; + + const b = ` + namespace NS { + export namespace Inner { + export const bar = "bar"; + } + } + `; + + util.testModule` + import './a'; + import './b'; + + export const result = NS.Inner; + ` + .addExtraFile("a.ts", a) + .addExtraFile("b.ts", b) + .expectToMatchJsResult(); +}); + +test("declared namespace function call", () => { + const luaHeader = ` + myNameSpace = {} + function myNameSpace.declaredFunction(x) return 3*x end + `; + + util.testModule` + declare namespace myNameSpace { + function declaredFunction(this: void, x: number): number; + } + + export const result = myNameSpace.declaredFunction(2); + ` + .setReturnExport("result") + .setLuaHeader(luaHeader) + .expectToEqual(6); +}); + +test("`import =` on a namespace", () => { + util.testModule` + namespace outerNamespace { + export namespace innerNamespace { + export function func() { + return "foo"; + } + } + } + + import importedFunc = outerNamespace.innerNamespace.func; + export const result = importedFunc(); + `.expectToMatchJsResult(); +}); + +test("enum in a namespace", () => { + util.testModule` + namespace Test { + export enum TestEnum { + A, + } + } + + export const result = Test.TestEnum.A; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1431 +test("nested namespaces (#1431)", () => { + util.testModule` + namespace Foo.Bar {} + namespace Foo.Bar { + export const baz = 32; + } + + export { Foo } + `.expectToMatchJsResult(); +}); diff --git a/test/unit/nullishCoalescing.spec.ts b/test/unit/nullishCoalescing.spec.ts new file mode 100644 index 000000000..56db55332 --- /dev/null +++ b/test/unit/nullishCoalescing.spec.ts @@ -0,0 +1,75 @@ +import * as util from "../util"; + +test.each(["null", "undefined"])("nullish-coalesing operator returns rhs", nullishValue => { + util.testExpression`${nullishValue} ?? "Hello, World!"` + .ignoreDiagnostics([2871 /* TS2871: This expression is always nullish. */]) + .expectToMatchJsResult(); +}); + +test.each([3, "foo", {}, [], true, false])("nullish-coalesing operator returns lhs", value => { + util.testExpression`${util.formatCode(value)} ?? "Hello, World!"` + .ignoreDiagnostics([ + 2869 /* TS2869: Right operand of ?? is unreachable because the left operand is never nullish. */, + ]) + .expectToMatchJsResult(); +}); + +test.each(["any", "unknown"])("nullish-coalesing operator with any/unknown type", type => { + util.testFunction` + const unknownType = false as ${type}; + return unknownType ?? "This should not be returned!"; + ` + .ignoreDiagnostics([2871 /* TS2871: This expression is always nullish. */]) + .expectToMatchJsResult(); +}); + +test.each(["boolean | string", "number | false", "undefined | true"])( + "nullish-coalesing operator with union type", + unionType => { + util.testFunction` + const unknownType = false as ${unionType}; + return unknownType ?? "This should not be returned!"; + `.expectToMatchJsResult(); + } +); + +test("nullish-coalescing operator with side effect lhs", () => { + util.testFunction` + let i = 0; + const incI = () => ++i; + return [i, incI() ?? 3, i]; + `.expectToMatchJsResult(); +}); + +test("nullish-coalescing operator with side effect rhs", () => { + util.testFunction` + let i = 0; + const incI = () => ++i; + return [i, undefined ?? incI(), i]; + ` + .ignoreDiagnostics([2871 /* TS2871: This expression is always nullish. */]) + .expectToMatchJsResult(); +}); + +test("nullish-coalescing operator with vararg", () => { + util.testFunction` + + function foo(...args: any[]){ + return args + } + function bar(...args: any[]) { + let x: boolean | undefined = false + const y = x ?? foo(...args) + } + return bar(1, 2) + `.expectToMatchJsResult(); +}); + +test.each([true, false, null])("nullish-coalescing with generic lhs (%p)", lhs => { + util.testFunction` + function coalesce(a: A, b: B) { + return a ?? b + } + return coalesce(${lhs}, "wasNull") + `.expectToMatchJsResult(); +}); diff --git a/test/unit/objectLiteral.spec.ts b/test/unit/objectLiteral.spec.ts index dd94ff419..16e7582d0 100644 --- a/test/unit/objectLiteral.spec.ts +++ b/test/unit/objectLiteral.spec.ts @@ -1,26 +1,128 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import * as util from "../src/util"; -const fs = require("fs"); - -export class ObjectLiteralTests { - - @TestCase(`{a:3,b:"4"}`, `{a = 3, b = "4"};`) - @TestCase(`{"a":3,b:"4"}`, `{a = 3, b = "4"};`) - @TestCase(`{["a"]:3,b:"4"}`, `{a = 3, b = "4"};`) - @TestCase(`{["a"+123]:3,b:"4"}`, `{["a" .. 123] = 3, b = "4"};`) - @TestCase(`{[myFunc()]:3,b:"4"}`, `{[myFunc()] = 3, b = "4"};`) - @TestCase(`{x}`, `{x = x};`) - @Test("Object Literal") - public objectLiteral(inp: string, out: string): void { - const lua = util.transpileString(`const myvar = ${inp};`); - Expect(lua).toBe(`local myvar = ${out}`); +import * as util from "../util"; + +test.each(['{ a: 3, b: "4" }', '{ "a": 3, b: "4" }', '{ ["a"]: 3, b: "4" }', '{ ["a" + 123]: 3, b: "4" }'])( + "Object Literal (%p)", + inp => { + util.testExpression(inp).expectToMatchJsResult(); } +); + +test("object literal with function call to get key", () => { + util.testFunction` + const myFunc = () => "a"; + return { [myFunc() + "b"]: 3 }; + `.expectToMatchJsResult(); +}); + +test("object literal with shorthand property", () => { + util.testFunction` + const x = 5; + return { x }; + `.expectToMatchJsResult(); +}); + +describe("property shorthand", () => { + test("should support property shorthand", () => { + util.testFunction` + const x = 1; + const o = { x }; + return o.x; + `.expectToMatchJsResult(); + }); + + test.each([NaN, Infinity])("should support %p shorthand", identifier => { + util.testExpression`({ ${identifier} }).${identifier}`.expectToMatchJsResult(); + }); + + test("should support _G shorthand", () => { + util.testExpression`({ _G })._G.foobar` + .setTsHeader("declare const _G: any;") + .setLuaHeader('foobar = "foobar"') + .expectToEqual("foobar"); + }); + + test("should support export property shorthand", () => { + util.testModule` + export const x = 1; + const o = { x }; + export const y = o.x; + `.expectToMatchJsResult(); + }); +}); - @TestCase("3", 3) - @Test("Shorthand Property Assignment") - public ShorthandPropertyAssignment(input: string, expected: number): void { - const result = util.transpileAndExecute(`const x = ${input}; const o = {x}; return o.x;`); - Expect(result).toBe(expected); +test("undefined as object key", () => { + util.testFunction` + const foo = {undefined: "foo"}; + return foo.undefined; + `.expectToMatchJsResult(); +}); + +test.each(['{x: "foobar"}.x', '{x: "foobar"}["x"]', '{x: () => "foobar"}.x()', '{x: () => "foobar"}["x"]()'])( + "object literal property access (%p)", + expression => { + util.testExpression(expression).expectToMatchJsResult(); } -} +); + +describe("noSelf in functions", () => { + test("Explicit this: void parameter", () => { + // language=TypeScript + util.testFunction` + const obj: Record string> = { + method(a) { + return a; + }, + func: function(a) { + return a; + }, + arrow: (a) => { + return a; + } + }; + return [obj.method("a") ?? "nil", obj.func("b") ?? "nil", obj.arrow("c") ?? "nil"]; + `.expectToMatchJsResult(); + }); + + test("No self annotation", () => { + // language=TypeScript + util.testFunction` + const obj: Record string> = { + method(a) { + return a; + }, + func: function(a) { + return a; + }, + arrow: (a) => { + return a; + } + }; + return [obj.method("a") ?? "nil", obj.func("b") ?? "nil", obj.arrow("c") ?? "nil"]; + `.expectToMatchJsResult(); + }); + + test("individual function types", () => { + // language=TypeScript + util.testFunction` + interface FunctionContainer { + method: (this: void, a: string) => string + func: (this: void, a: string) => string + arrow: (this: void, a: string) => string + } + + const obj: FunctionContainer = { + method(a) { + return a + }, + func: function(a) { + return a + }, + arrow: (a) => { + return a + } + } + + return [obj.method("a") ?? "nil", obj.func("b") ?? "nil", obj.arrow("c") ?? "nil"]; + `.expectToMatchJsResult(); + }); +}); diff --git a/test/unit/optionalChaining.spec.ts b/test/unit/optionalChaining.spec.ts new file mode 100644 index 000000000..d99f18be9 --- /dev/null +++ b/test/unit/optionalChaining.spec.ts @@ -0,0 +1,459 @@ +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}; + return obj?.foo; + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + // should use "and" expression +}); + +test("long optional chain", () => { + util.testFunction` + const a = { b: { c: { d: { e: { f: "hello!"}}}}}; + return a.b?.c?.d.e.f; + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + // should use "and" expression +}); + +test.each(["undefined", "{}", "{ foo: {} }", "{ foo: {bar: 'baz'}}"])("nested optional chaining (%p)", value => { + util.testFunction` + const obj: { foo?: { bar?: string } } | undefined = ${value}; + return obj?.foo?.bar; + `.expectToMatchJsResult(); +}); + +test.each(["undefined", "{}", "{ foo: {} }", "{ foo: {bar: 'baz'}}"])( + "nested optional chaining combined with coalescing (%p)", + value => { + util.testFunction` + const obj: { foo?: { bar?: string } } | undefined = ${value}; + return obj?.foo?.bar ?? "not found"; + `.expectToMatchJsResult(); + } +); + +test.each(["[1, 2, 3, 4]", "undefined"])("optional array access (%p)", value => { + util.testFunction` + const arr: number[] | undefined = ${value}; + return arr?.[2]; + `.expectToMatchJsResult(); +}); + +test.each(["[1, [2, [3, [4, 5]]]]", "[1, [2, [3, undefined]]] ", "[1, undefined]"])( + "optional element access nested (%p)", + value => { + util.testFunction` + const arr: [number, [number, [number, [number, number] | undefined]]] | [number, undefined] = ${value}; + return arr[1]?.[1][1]?.[0]; + `.expectToMatchJsResult(); + } +); + +test.each(["{ }", "{ a: { } }", "{ a: { b: [{ c: 10 }] } }"])( + "optional nested element access properties (%p)", + value => { + util.testFunction` + const obj: {a?: {b?: Array<{c: number }> } } = ${value}; + return [obj["a"]?.["b"]?.[0]?.["c"] ?? "not found", obj["a"]?.["b"]?.[2]?.["c"] ?? "not found"]; + `.expectToMatchJsResult(); + } +); + +test("optional element function calls", () => { + util.testFunction` + const obj: { value: string; foo?(this: void, v: number): number; bar?(this: void, v: number): number; } = { + value: "foobar", + foo: (v: number) => v + 10 + } + const fooKey = "foo"; + const barKey = "bar"; + return obj[barKey]?.(5) ?? obj[fooKey]?.(15); + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + // should still use "and" statement, as functions have no self +}); + +test("unused expression", () => { + util.testFunction` + const obj = { foo: "bar" }; + obj?.foo; + ` + .expectToHaveNoDiagnostics() + .expectNoExecutionError() + .expectLuaToMatchSnapshot(); + // should use if statement, as result is not used +}); + +test("unused call", () => { + util.testFunction` + let result + const obj = { + foo() { + result = "bar" + } + }; + obj?.foo(); + return result; + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + // should use if statement, as result is not used +}); + +test.each(["undefined", "{ foo: v=>v }"])("with preceding statements on right side", value => { + util.testFunction` + let i = 0 + const obj: any = ${value}; + return {result: obj?.foo(i++), i}; + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + // should use if statement, as there are preceding statements +}); + +// unused, with preceding statements on right side +test.each(["undefined", "{ foo(val) {return val} }"])( + "unused result with preceding statements on right side", + value => { + util.testFunction` + let i = 0 + const obj = ${value}; + obj?.foo(i++); + return i + ` + .expectToHaveNoDiagnostics() + .expectLuaToMatchSnapshot(); + // should use if statement, as there are preceding statements + } +); + +test.each(["undefined", "{ foo(v) { return v} }"])("with preceding statements on right side modifying left", value => { + util.testFunction` + let i = 0 + let obj: any = ${value}; + function bar() { + if(obj) obj.foo = undefined + obj = undefined + return 1 + } + + return {result: obj?.foo(bar(), i++), obj, i} + ` + .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` + const obj: any = false + return obj?.foo + `.getLuaExecutionResult(); + expect(result).toBeInstanceOf(util.ExecutionError); +}); + +describe("optional access method calls", () => { + test("element access call", () => { + util.testFunction` + const obj: { value: string; foo?(prefix: string): string; bar?(prefix: string): string; } = { + value: "foobar", + foo(prefix: string) { return prefix + this.value; } + } + const fooKey = "foo"; + const barKey = "bar"; + return obj[barKey]?.("bar?") ?? obj[fooKey]?.("foo?"); + `.expectToMatchJsResult(); + }); + + test("property access call", () => { + util.testFunction` + const obj: { value: string; foo?(prefix: string): string; bar?(prefix: string): string; } = { + value: "foobar", + foo(prefix: string) { return prefix + this.value; } + } + return obj.foo?.("bar?") ?? obj.bar?.("foo?"); + `.expectToMatchJsResult(); + }); + + test("nested optional element access call", () => { + util.testFunction` + const obj: { value: string; foo?(prefix: string): string; bar?(prefix: string): string; } = { + value: "foobar", + foo(prefix: string) { return prefix + this.value; } + } + const fooKey = "foo"; + const barKey = "bar"; + return obj?.[barKey]?.("bar?") ?? obj?.[fooKey]?.("foo?"); + `.expectToMatchJsResult(); + }); + + test("nested optional property access call", () => { + util.testFunction` + const obj: { value: string; foo?(prefix: string): string; bar?(prefix: string): string; } = { + value: "foobar", + foo(prefix: string) { return prefix + this.value; } + } + return obj?.foo?.("bar?") ?? obj?.bar?.("foo?"); + `.expectToMatchJsResult(); + }); +}); + +test("no side effects", () => { + util.testFunction` + function getFoo(): { foo: number } | undefined { + return { foo: 42 }; + } + let barCalls = 0; + function getBar(): { bar: number } | undefined { + barCalls += 1; + return undefined; + } + const result = getFoo()?.foo ?? getBar()?.bar; + return { result, barCalls }; + `.expectToMatchJsResult(); +}); + +// Test for https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1044 +test("does not crash when incorrectly used in assignment (#1044)", () => { + const { diagnostics } = util.testFunction` + foo?.bar = "foo"; + `.getLuaResult(); + + expect(diagnostics.find(d => d.code === notAllowedOptionalAssignment.code)).toBeDefined(); +}); + +describe("optional chaining function calls", () => { + test.each(["() => 4", "undefined"])("stand-alone optional function (%p)", value => { + util.testFunction` + const f: (() => number) | undefined = ${value}; + return f?.(); + `.expectToMatchJsResult(); + }); + + test("methods present", () => { + util.testFunction` + const objWithMethods = { + foo() { + return 3; + }, + bar(this: void) { + return 5; + } + }; + + return [objWithMethods?.foo(), objWithMethods?.bar()]; + `.expectToMatchJsResult(); + }); + + test("object with method can be undefined", () => { + util.testFunction` + const objWithMethods: { foo: () => number, bar: (this: void) => number } | undefined = undefined; + return [objWithMethods?.foo() ?? "no foo", objWithMethods?.bar() ?? "no bar"]; + `.expectToMatchJsResult(); + }); + + test("nested optional method call", () => { + util.testFunction` + type typeWithOptional = { a?: { b: { c: () => number } } }; + + const objWithMethods: typeWithOptional = {}; + const objWithMethods2: typeWithOptional = { a: { b: { c: () => 4 } } }; + + return { + expectNil: objWithMethods.a?.b.c(), + expectFour: objWithMethods2.a?.b.c() + }; + `.expectToMatchJsResult(); + }); + + test("methods are undefined", () => { + util.testFunction` + const objWithMethods: { foo?: () => number, bar?: (this: void) => number } = {}; + return [objWithMethods.foo?.() ?? "no foo", objWithMethods.bar?.() ?? "no bar"]; + `.expectToMatchJsResult(); + }); + + test("optional method of optional method result", () => { + util.testFunction` + const obj: { a?: () => {b: {c?: () => number }}} = {}; + const obj2: { a?: () => {b: {c?: () => number }}} = { a: () => ({b: {}})}; + const obj3: { a?: () => {b: {c?: () => number }}} = { a: () => ({b: { c: () => 5 }})}; + + return [obj.a?.().b.c?.() ?? "nil", obj2.a?.().b.c?.() ?? "nil", obj3.a?.().b.c?.() ?? "nil"]; + `.expectToMatchJsResult(); + }); + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1085 + test("incorrect type, method is not a function", () => { + util.testFunction` + const obj: any = {}; + obj?.foo(); + `.expectToEqual(new util.ExecutionError("attempt to call a nil value (method 'foo')")); + }); + + describe("builtins", () => { + test.each([ + ["undefined", undefined], + ["{foo: 0}", true], + ])("LuaTable: %p", (expr, value) => { + util.testFunction` + const table: LuaTable = ${expr} as any + const bar = table?.has("foo") + return bar + ` + .withLanguageExtensions() + .expectToEqual(value); + }); + + test.each(["undefined", "foo"])("Function call: %p", e => { + util.testFunction` + const result = [] + function foo(this: unknown, arg: unknown) { + return [this, arg] + } + const bar = ${e} as typeof foo | undefined + return bar?.call(1, 2) + `.expectToMatchJsResult(); + }); + + test.each([undefined, "[1, 2, 3, 4]"])("Array: %p", expr => { + util.testFunction` + const value: any[] | undefined = ${expr} + return value?.map(x=>x+1) + `.expectToMatchJsResult(); + }); + }); + + test.each([true, false])("Default call context, strict %s", strict => { + util.testFunction` + function func(this: unknown, arg: unknown) { + return [this === globalThis ? "_G" : this === undefined ? "nil" : "neither", arg]; + } + let i = 0 + const result = func?.(i++); + ` + .setOptions({ + strict, + target: ScriptTarget.ES5, + }) + .expectToMatchJsResult(); + }); +}); + +describe("Unsupported optional chains", () => { + test("Language extensions", () => { + util.testModule` + new LuaTable().has?.(3) + ` + .withLanguageExtensions() + .expectDiagnosticsToMatchSnapshot(); + }); + + test("Builtin prototype method", () => { + util.testModule` + [1,2,3,4].forEach?.(()=>{}) + `.expectDiagnosticsToMatchSnapshot(); + }); + + test("Builtin global method", () => { + util.testModule` + Number?.("3") + `.expectDiagnosticsToMatchSnapshot(); + }); + + test("Builtin global property", () => { + util.testModule` + console?.log("3") + ` + .setOptions({ + lib: ["lib.esnext.d.ts", "lib.dom.d.ts"], + }) + .expectDiagnosticsToMatchSnapshot(); + }); + + test("Compile members only", () => { + util.testFunction` + /** @compileMembersOnly */ + enum TestEnum { + A = 0, + B = 2, + C, + D = "D", + } + + TestEnum?.B + `.expectDiagnosticsToMatchSnapshot(); + }); +}); + +describe("optional delete", () => { + test("successful", () => { + util.testFunction` + const table = { + bar: 3 + } + return [delete table?.bar, table] + `.expectToMatchJsResult(); + }); + + test("unsuccessful", () => { + util.testFunction` + const table : { + bar?: number + } = {} + return [delete table?.bar, table] + `.expectToMatchJsResult(); + }); + + test("delete on undefined", () => { + util.testFunction` + const table : { + bar: number + } | undefined = undefined + return [delete table?.bar, table ?? "nil"] + `.expectToMatchJsResult(); + }); +}); + +describe("Non-null chain", () => { + test("Single non-null chain", () => { + util.testFunction` + const foo = {a: { b: 3} } + return foo?.a!.b + `.expectToMatchJsResult(); + }); + + test("Many non-null chains", () => { + util.testFunction` + const foo = {a: { b: 3} } + return foo?.a!!!.b!!! + `.expectToMatchJsResult(); + }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1585 +test("optional chaining of super call (#1585)", () => { + util.testFunction` + class Parent { + private name = "foo"; + M2() { return this.name; } + } + + class Child extends Parent { + M2() { + return super.M2?.(); + } + } + + const c = new Child(); + return c.M2(); + `.expectToMatchJsResult(); +}); diff --git a/test/unit/overloads.spec.ts b/test/unit/overloads.spec.ts index 1b6d7e925..d18daa000 100644 --- a/test/unit/overloads.spec.ts +++ b/test/unit/overloads.spec.ts @@ -1,129 +1,107 @@ -import { Expect, Test, TestCase } from "alsatian"; +import * as util from "../util"; -import * as util from "../src/util"; +test("overload function1", () => { + util.testFunction` + function abc(def: number): string; + function abc(def: string): string; + function abc(def: number | string): string { + if (typeof def == "number") { + return "jkl" + (def * 3); + } else { + return def; + } + } + return abc(3); + `.expectToMatchJsResult(); +}); + +test("overload function2", () => { + util.testFunction` + function abc(def: number): string; + function abc(def: string): string; + function abc(def: number | string): string { + if (typeof def == "number") { + return "jkl" + (def * 3); + } else { + return def; + } + } + return abc("ghj"); + `.expectToMatchJsResult(); +}); -export class OverloadTests { - @Test("overload function1") - public overloadFunction1(): void - { - const result = util.transpileAndExecute( - `function abc(def: number): string; - function abc(def: string): string; - function abc(def: number | string): string { +test("overload method1", () => { + util.testFunction` + class myclass { + static abc(def: number): string; + static abc(def: string): string; + static abc(def: number | string): string { if (typeof def == "number") { return "jkl" + (def * 3); } else { return def; } } - return abc(3);`); + } + return myclass.abc(3); + `.expectToMatchJsResult(); +}); - Expect(result).toBe("jkl9"); - } - - @Test("overload function2") - public overloadFunction2(): void - { - const result = util.transpileAndExecute( - `function abc(def: number): string; - function abc(def: string): string; - function abc(def: number | string): string { +test("overload method2", () => { + util.testFunction` + class myclass { + static abc(def: number): string; + static abc(def: string): string; + static abc(def: number | string): string { if (typeof def == "number") { return "jkl" + (def * 3); } else { return def; } } - return abc("ghj");`); - - Expect(result).toBe("ghj"); - } - - @Test("overload method1") - public overloadMethod1(): void - { - const result = util.transpileAndExecute( - `class myclass { - static abc(def: number): string; - static abc(def: string): string; - static abc(def: number | string): string { - if (typeof def == "number") { - return "jkl" + (def * 3); - } else { - return def; - } - } - } - return myclass.abc(3);`); + } + return myclass.abc("ghj"); + `.expectToMatchJsResult(); +}); - Expect(result).toBe("jkl9"); - } +test("constructor1", () => { + util.testFunction` + class myclass { + num: number; + str: string; - @Test("overload method2") - public overloadMethod2(): void - { - const result = util.transpileAndExecute( - `class myclass { - static abc(def: number): string; - static abc(def: string): string; - static abc(def: number | string): string { - if (typeof def == "number") { - return "jkl" + (def * 3); - } else { - return def; - } - } - } - return myclass.abc("ghj");`); - - Expect(result).toBe("ghj"); - } - - @Test("constructor1") - public constructor1(): void - { - const result = util.transpileAndExecute( - `class myclass { - num: number; - str: string; - - constructor(def: number); - constructor(def: string); - constructor(def: number | string) { - if (typeof def == "number") { - this.num = def; - } else { - this.str = def; - } + constructor(def: number); + constructor(def: string); + constructor(def: number | string) { + if (typeof def == "number") { + this.num = def; + } else { + this.str = def; } } - const inst = new myclass(3); - return inst.num`); + } + const inst = new myclass(3); + return inst.num; + `.expectToMatchJsResult(); +}); - Expect(result).toBe(3); - } +test("constructor2", () => { + util.testFunction` + class myclass { + num: number; + str: string; - @Test("constructor2") - public constructor2(): void - { - const result = util.transpileAndExecute( - `class myclass { - num: number; - str: string; - - constructor(def: number); - constructor(def: string); - constructor(def: number | string) { - if (typeof def == "number") { - this.num = def; - } else { - this.str = def; - } + constructor(def: number); + constructor(def: string); + constructor(def: number | string) { + if (typeof def == "number") { + this.num = def; + } else { + this.str = def; } } - const inst = new myclass("ghj"); - return inst.str`); - - Expect(result).toBe("ghj"); - } -} + } + const inst = new myclass("ghj"); + return inst.str + `.expectToMatchJsResult(); +}); diff --git a/test/unit/precedingStatements.spec.ts b/test/unit/precedingStatements.spec.ts new file mode 100644 index 000000000..1ce24da19 --- /dev/null +++ b/test/unit/precedingStatements.spec.ts @@ -0,0 +1,656 @@ +import * as util from "../util"; + +const shortCircuitTests: Array<{ operator: string; testValue: unknown }> = [ + { operator: "&&", testValue: true }, + { operator: "&&", testValue: false }, + { operator: "&&", testValue: null }, + { operator: "&&=", testValue: true }, + { operator: "&&=", testValue: false }, + { operator: "&&=", testValue: null }, + { operator: "||", testValue: true }, + { operator: "||", testValue: false }, + { operator: "||", testValue: null }, + { operator: "||=", testValue: true }, + { operator: "||=", testValue: false }, + { operator: "||=", testValue: null }, + { operator: "??", testValue: true }, + { operator: "??", testValue: false }, + { operator: "??", testValue: null }, + { operator: "??=", testValue: true }, + { operator: "??=", testValue: false }, + { operator: "??=", testValue: null }, +]; + +test.each(shortCircuitTests)("short circuit operator (%p)", ({ operator, testValue }) => { + util.testFunction` + let x: unknown = ${testValue}; + let y = 1; + const z = x ${operator} y++; + return {x, y, z}; + `.expectToMatchJsResult(); +}); + +test.each(shortCircuitTests)("short circuit operator on property (%p)", ({ operator, testValue }) => { + util.testFunction` + let x: { foo: unknown } = { foo: ${testValue} }; + let y = 1; + const z = x.foo ${operator} y++; + return {x: x.foo, y, z}; + `.expectToMatchJsResult(); +}); + +test.each([true, false])("ternary operator (%p)", condition => { + util.testFunction` + let a = 0, b = 0; + let condition: boolean = ${condition}; + const c = condition ? a++ : b++; + return [a, b, c]; + `.expectToMatchJsResult(); +}); + +describe("execution order", () => { + const sequenceTests = [ + "i++, i", + "i, i++, i, i++", + "...a", + "i, ...a", + "...a, i", + "i, ...a, i++, i, ...a", + "i, ...a, i++, i, ...a, i", + "...[1, i++, 2]", + "...[1, i++, 2], i++", + "i, ...[1, i++, 2]", + "i, ...[1, i++, 2], i", + "i, ...[1, i++, 2], i++", + "i, ...[1, i++, 2], i++, ...[3, i++, 4]", + "i, ...a, i++, ...[1, i++, 2], i, i++, ...a", + "i, inc(), i++", + "i, ...[1, i++, inc(), 2], i++", + "i, ...'foo', i++", + "i, ...([1, i++, 2] as any), i++", + ]; + + test.each(sequenceTests)("array literal ([%p])", sequence => { + util.testFunction` + const a = [7, 8, 9]; + let i = 0; + function inc() { ++i; return i; } + return [${sequence}]; + `.expectToMatchJsResult(); + }); + + test.each(sequenceTests)("function arguments (foo(%p))", sequence => { + util.testFunction` + const a = [7, 8, 9]; + let i = 0; + function inc() { ++i; return i; } + function foo(...args: unknown[]) { return args; } + return foo(${sequence}); + `.expectToMatchJsResult(); + }); + + test.each([ + "{a: i, b: i++}", + "{a: i, b: i++, c: i}", + "{a: i, ...{b: i++}, c: i}", + "{a: i, ...o, b: i++}", + "{a: i, ...[i], b: i++}", + "{a: i, ...[i++], b: i++}", + "{a: i, ...o, b: i++, ...[i], ...{c: i++}, d: i++}", + ])("object literal (%p)", literal => { + util.testFunction` + const o = {a: "A", b: "B", c: "C"}; + let i = 0; + const literal = ${literal}; + const result: Record = {}; + (Object.keys(result) as Array).forEach( + key => { result[key.toString()] = literal[key]; } + ); + return result; + `.expectToMatchJsResult(); + }); + + test("object literal with computed property names", () => { + util.testFunction` + let i = "A"; + const o = {[i += "B"]: i += "C", [i += "D"]: i += "E"}; + return [i, o]; + `.expectToMatchJsResult(); + }); + + test("comma operator", () => { + util.testFunction` + let a = 0, b = 0, c = 0; + const d = (a++, b += a, c += b); + return [a, b, c, d]; + `.expectToMatchJsResult(); + }); + + test("template expression", () => { + util.testFunction` + let i = 0; + return \`\${i}, \${i++}, \${i}\`; + `.expectToMatchJsResult(); + }); + + test("tagged template literal", () => { + util.testFunction` + let i = 0; + + function func(strings: TemplateStringsArray, ...expressions: any[]) { + const x = i > 0 ? "a" : "b"; + return { strings: [x, ...strings], raw: strings.raw, expressions }; + } + + return func\`hello \${i} \${i++}\`; + `.expectToMatchJsResult(); + }); + + test("binary operators", () => { + util.testFunction` + let i = 0; + return i + i++; + `.expectToMatchJsResult(); + }); + + test("index expression", () => { + util.testFunction` + let i = 0; + const a = [["A1", "A2"], ["B1", "B2"]]; + const result = a[i][i++]; + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("void expression", () => { + util.testFunction` + function foo(x: number, y: number, z: number | undefined) { + return z; + } + let i = 0; + const result = foo(i, i++, void(i++)); + return {result, i}; + `.expectToMatchJsResult(); + }); +}); + +describe("assignment execution order", () => { + test("index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [4, 5]; + a[i] = i++; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const result = a[i] = i++; + return [result, a, i]; + `.expectToMatchJsResult(); + }); + + test("indirect index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + foo(i)[i] = i++; + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + test("indirect index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i)[i] = i++; + return [result, a, b, i]; + `.expectToMatchJsResult(); + }); + + test("indirect property assignment statement", () => { + util.testFunction` + const a = {value: 10}; + const b = {value: 11}; + let i = 0; + function foo(x: number) { return (x > 0) ? b : a; } + foo(i).value = i++; + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + test("indirect property assignment expression", () => { + util.testFunction` + const a = {value: 10}; + const b = {value: 11}; + let i = 0; + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i).value = i++; + return [result, a, b, i]; + `.expectToMatchJsResult(); + }); + + test("compound index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + a[i] += i++; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("compound index assignment expression", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const result = a[i] += i++; + return [result, a, i]; + `.expectToMatchJsResult(); + }); + + test("compound indirect index assignment statement", () => { + util.testFunction` + let i = 0; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + foo(i)[i] += i++; + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + test("compound indirect index assignment expression", () => { + util.testFunction` + let i = 1; + const a = [9, 8]; + const b = [7, 6]; + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i)[i] += i++; + return [result, a, b, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment statement", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + [a[i], a[i++]] = [i++, i++]; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment expression", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + const result = [a[i], a[i++]] = [i++, i++]; + return [a, i, result]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment statement with default", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + [a[i] = i++, a[i++]] = [i++, i++]; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment expression with default", () => { + util.testFunction` + const a = [10, 9, 8, 7, 6, 5]; + let i = 0; + const result = [a[i] = i++, a[i++]] = [i++, i++]; + return [a, i, result]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment statement with spread", () => { + util.testFunction` + let i = 0; + let a: number[][] = [[9, 9, 9], [9, 9, 9], [9, 9, 9]]; + [a[0][i], ...a[i++]] = [i++, i++]; + return [a, i]; + `.expectToMatchJsResult(); + }); + + test("array destructuring assignment expression with spread", () => { + util.testFunction` + let i = 0; + let a: number[][] = [[9, 9, 9], [9, 9, 9], [9, 9, 9]]; + const result = [a[0][i], ...a[i++]] = [i++, i++]; + return [a, i, result]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment statement", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDEFG: "success", result: ""}; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function e(x: string) { s = x + "E"; return s; } + ({ [e(s += "D")]: g(s += "F").result } = c(s += "B")); + return [s, o]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment statement with default", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDEFGHIJ: "success", result: ""}; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function i(x: string) { s = x + "I"; return o; } + function e(x: string): any { s = x + "E"; return undefined; } + ({ [e(s += "D")]: g(s += "F").result = i(s += "H")[s += "J"] } = c(s += "B")); + return [o, s]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment expression", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDEFG: "success", result: ""}; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function e(x: string) { s = x + "E"; return s; } + const result = ({ [e(s += "D")]: g(s += "F").result } = c(s += "B")); + return [s, o, result]; + `.expectToMatchJsResult(); + }); + + test("object destructuring assignment expression with default", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDEFGHIJ: "success", result: ""}; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function i(x: string) { s = x + "I"; return o; } + function e(x: string): any { s = x + "E"; return undefined; } + const result = ({ [e(s += "D")]: g(s += "F").result = i(s += "H")[s += "J"] } = c(s += "B")); + return [o, s, result]; + `.expectToMatchJsResult(); + }); + + test("object destructuring declaration", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDE: "success"}; + function c(x: string) { s = x + "C"; return o; } + function e(x: string) { s = x + "E"; return s; } + const { [e(s += "D")]: result } = c(s += "B"); + return [result, s]; + `.expectToMatchJsResult(); + }); + + test("object destructuring declaration with default", () => { + util.testFunction` + let s = "A"; + const o: Record = {ABCDEFGH: "success"}; + function c(x: string) { s = x + "C"; return o; } + function g(x: string) { s = x + "G"; return o; } + function e(x: string): any { s = x + "E"; return undefined; } + const { [e(s += "D")]: result = g(s += "F")[s += "H"]} = c(s += "B"); + return [result, s]; + `.expectToMatchJsResult(); + }); + + test("call expression", () => { + util.testFunction` + let i = 1; + function a(x: number) { return x * 10; } + function b(x: number) { return x * 100; } + function foo(x: number) { return (x > 0) ? b : a; } + const result = foo(i)(i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("call expression (function modified)", () => { + util.testFunction` + let i = 1; + let foo = (x: null, y: number) => { return y; }; + function changeFoo() { + foo = (x: null, y: number) => { return y * 10; }; + return null; + } + const result = foo(changeFoo(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method call expression (method modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeFoo(this: void) { + o.foo = function(x: null, y: number) { return (y + this.val) * 10; }; + return null; + } + const result = o.foo(changeFoo(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method element access call expression (method modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeFoo(this: void) { + o.foo = function(x: null, y: number) { return (y + this.val) * 10; }; + return null; + } + function getFoo() { return "foo" as const; } + function getO() { return o; } + const result = getO()[getFoo()](changeFoo(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method call expression (object modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeO(this: void) { + o = { + val: 5, + foo: function(x: null, y: number) { return (y + this.val) * 10; } + }; + return null; + } + const result = o.foo(changeO(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("method element access call expression (object modified)", () => { + util.testFunction` + let i = 1; + let o = { + val: 3, + foo(x: null, y: number) { return y + this.val; } + }; + function changeO(this: void) { + o = { + val: 5, + foo: function(x: null, y: number) { return (y + this.val) * 10; } + }; + return null; + } + function getFoo() { return "foo" as const; } + function getO() { return o; } + const result = getO()[getFoo()](changeO(), i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("array method call", () => { + util.testFunction` + let a = [7]; + let b = [9]; + function foo(x: number) { return (x > 0) ? b : a; } + let i = 0; + foo(i).push(i, i++, i); + return [a, b, i]; + `.expectToMatchJsResult(); + }); + + 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; }; + function foo(x: number) { return (x > 0) ? b : a; } + let i = 0; + const result = foo(i).call(o, i++); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("string method call", () => { + util.testFunction` + function foo(x: number) { return (x > 0) ? "foo" : "bar"; } + let i = 0; + const result = foo(i).substr(++i); + return [result, i]; + `.expectToMatchJsResult(); + }); + + test("new call", () => { + util.testFunction` + class A { public val = 3; constructor(x: number) { this.val += x; } }; + class B { public val = 5; constructor(x: number) { this.val += (x * 10); } }; + function foo(x: number) { return (x > 0) ? B : A; } + let i = 0; + const result = new (foo(i))(i++).val; + return [result, i]; + `.expectToMatchJsResult(); + }); +}); + +describe("loop expressions", () => { + test("while loop", () => { + util.testFunction` + let i = 0, j = 0; + while (i++ < 5) { + ++j; + if (j >= 10) { + break; + } + } + return [i, j]; + `.expectToMatchJsResult(); + }); + + test("for loop", () => { + util.testFunction` + let j = 0; + for (let i = 0; i++ < 5;) { + ++j; + if (j >= 10) { + break; + } + } + return j; + `.expectToMatchJsResult(); + }); + + test("do while loop", () => { + util.testFunction` + let i = 0, j = 0; + do { + ++j; + if (j >= 10) { + break; + } + } while (i++ < 5); + return [i, j]; + `.expectToMatchJsResult(); + }); + + test("do while loop scoping", () => { + util.testFunction` + let x = 0; + let result = 0; + do { + let x = -10; + ++result; + } while (x++ >= 0 && result < 2); + return result; + `.expectToMatchJsResult(); + }); +}); + +test("switch", () => { + util.testFunction` + let i = 0; + let x = 0; + let result = ""; + switch (x) { + case i++: + result = "test"; + break; + case i++: + } + return [i, result]; + `.expectToMatchJsResult(); +}); + +test("else if", () => { + util.testFunction` + let i = 0; + if (i++ === 0) { + } else if (i++ === 1) { + } + return i; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1208 +test("class member initializers", () => { + util.testFunction` + class MyClass { + myField = false ?? true; + constructor(public foo: number = 0 ?? 5) {} + } + const inst = new MyClass(); + return [inst.myField, inst.foo]; + ` + .ignoreDiagnostics([ + 2869 /* TS2869: Right operand of ?? is unreachable because the left operand is never nullish. */, + ]) + .expectToMatchJsResult(); +}); + +test("class member initializers in extended class", () => { + util.testFunction` + class A {} + class MyClass extends A { + myField = false ?? true; + } + const inst = new MyClass(); + return inst.myField; + ` + .ignoreDiagnostics([ + 2869 /* TS2869: Right operand of ?? is unreachable because the left operand is never nullish. */, + ]) + .expectToMatchJsResult(); +}); diff --git a/test/unit/printer/__snapshots__/semicolons.spec.ts.snap b/test/unit/printer/__snapshots__/semicolons.spec.ts.snap new file mode 100644 index 000000000..ac34dbdf8 --- /dev/null +++ b/test/unit/printer/__snapshots__/semicolons.spec.ts.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`semicolon insertion ("{}") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local result = "" + local function foo(self) + result = "foo" + end + do + end + (nil or foo)(nil) + return result +end +return ____exports" +`; + +exports[`semicolon insertion ("const a = 1; const b = a;") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local result = "" + local function foo(self) + result = "foo" + end + local a = 1 + local b = a; + (nil or foo)(nil) + return result +end +return ____exports" +`; + +exports[`semicolon insertion ("const a = 1; let b: number; b = a;") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local result = "" + local function foo(self) + result = "foo" + end + local a = 1 + local b + b = a; + (nil or foo)(nil) + return result +end +return ____exports" +`; + +exports[`semicolon insertion ("function bar() {} bar();") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local result = "" + local function foo(self) + result = "foo" + end + local function bar(self) + end + bar(nil); + (nil or foo)(nil) + return result +end +return ____exports" +`; diff --git a/test/unit/printer/deadCodeAfterReturn.spec.ts b/test/unit/printer/deadCodeAfterReturn.spec.ts new file mode 100644 index 000000000..fba16387a --- /dev/null +++ b/test/unit/printer/deadCodeAfterReturn.spec.ts @@ -0,0 +1,61 @@ +import * as util from "../../util"; + +test("If dead code after return", () => { + util.testFunction` + if (true) { + return 3; + const b = 8; + } + `.expectToMatchJsResult(); +}); + +test("switch dead code after return", () => { + util.testFunction` + switch ("abc" as string) { + case "def": + return 4; + let abc = 4; + case "abc": + return 5; + let def = 6; + } + `.expectToMatchJsResult(); +}); + +test("Function dead code after return", () => { + util.testFunction` + function abc() { return 3; const a = 5; } + return abc(); + `.expectToMatchJsResult(); +}); + +test("Method dead code after return", () => { + util.testFunction` + class def { public static abc() { return 3; const a = 5; } } + return def.abc(); + `.expectToMatchJsResult(); +}); + +test("for dead code after return", () => { + util.testFunction` + for (let i = 0; i < 10; i++) { return 3; const b = 8; } + `.expectToMatchJsResult(); +}); + +test("for..in dead code after return", () => { + util.testFunction` + for (let a in {"a": 5, "b": 8}) { return 3; const b = 8; } + `.expectToMatchJsResult(); +}); + +test("for..of dead code after return", () => { + util.testFunction` + for (let a of [1,2,4]) { return 3; const b = 8; } + `.expectToMatchJsResult(); +}); + +test("while dead code after return", () => { + util.testFunction` + while (true) { return 3; const b = 8; } + `.expectToMatchJsResult(); +}); diff --git a/test/unit/printer/parenthesis.spec.ts b/test/unit/printer/parenthesis.spec.ts new file mode 100644 index 000000000..6c8e58156 --- /dev/null +++ b/test/unit/printer/parenthesis.spec.ts @@ -0,0 +1,63 @@ +import * as util from "../../util"; + +test("binary expression with 'as' type assertion wrapped in parenthesis", () => { + util.testFunction("return 2 * (3 - 2 as number);").expectToMatchJsResult(); +}); + +test.each([ + "(x as any).foo;", + "(y.x as any).foo;", + "(y['x'] as any).foo;", + "(z() as any).foo;", + "(y.z() as any).foo;", + "(x).foo;", + "(y.x).foo;", + "(y['x']).foo;", + "(z()).foo;", + "(y.z()).foo;", + "(x as unknown as any).foo;", + "(x as any).foo;", + "((x as unknown) as any).foo;", + "((x) as any).foo;", +])("'as' type assertion should strip parenthesis (%p)", expression => { + const code = ` + declare let x: unknown; + declare let y: { x: unknown; z(this: void): unknown; }; + declare function z(this: void): unknown; + ${expression}`; + + const lua = util.testExpression(code).getMainLuaCodeChunk(); + expect(lua).not.toMatch(/\(.+\)/); +}); + +test.each([ + "(x + 1 as any).foo;", + "(!x as any).foo;", + "(x ** 2 as any).foo;", + "(x < 2 as any).foo;", + "(x in y as any).foo;", + "(x + 1).foo;", + "(!x).foo;", + "(x + 1 as unknown as any).foo;", + "((x + 1 as unknown) as any).foo;", + "(!x as unknown as any).foo;", + "((!x as unknown) as any).foo;", + "(!x as any).foo;", + "((!x) as any).foo;", +])("'as' type assertion should not strip parenthesis (%p)", expression => { + const code = ` + declare let x: number; + declare let y: {}; + ${expression}`; + + const lua = util.testExpression(code).getMainLuaCodeChunk(); + expect(lua).toMatch(/\(.+\)/); +}); + +test("not operator precedence (%p)", () => { + util.testFunction` + const a = true; + const b = false; + return !a && b; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/printer/semicolons.spec.ts b/test/unit/printer/semicolons.spec.ts new file mode 100644 index 000000000..7be461d0f --- /dev/null +++ b/test/unit/printer/semicolons.spec.ts @@ -0,0 +1,17 @@ +import * as util from "../../util"; + +test.each(["const a = 1; const b = a;", "const a = 1; let b: number; b = a;", "{}", "function bar() {} bar();"])( + "semicolon insertion (%p)", + leadingStatement => { + util.testFunction` + let result = ""; + function foo() { result = "foo"; } + ${leadingStatement} + (undefined || foo)(); + return result; + ` + .ignoreDiagnostics([2873 /* TS2873: This kind of expression is always falsy. */]) + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + } +); diff --git a/test/unit/printer/sourcemaps.spec.ts b/test/unit/printer/sourcemaps.spec.ts new file mode 100644 index 000000000..478f482d6 --- /dev/null +++ b/test/unit/printer/sourcemaps.spec.ts @@ -0,0 +1,350 @@ +import { SourceMapConsumer } from "source-map"; +import * as tstl from "../../../src"; +import { LuaTarget, transpileString } from "../../../src"; +import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; +import * as util from "../../util"; +import { lineAndColumnOf } from "./utils"; + +test.each([ + { + code: ` + const abc = "foo"; + const def = "bar"; + + const xyz = "baz"; + `, + + assertPatterns: [ + { luaPattern: "abc", typeScriptPattern: "abc" }, + { luaPattern: "def", typeScriptPattern: "def" }, + { luaPattern: "xyz", typeScriptPattern: "xyz" }, + { luaPattern: '"foo"', typeScriptPattern: '"foo"' }, + { luaPattern: '"bar"', typeScriptPattern: '"bar"' }, + { luaPattern: '"baz"', typeScriptPattern: '"baz"' }, + ], + }, + { + code: ` + function abc() { + return def(); + } + + function def() { + return "foo"; + } + `, + + assertPatterns: [ + { luaPattern: "function abc(", typeScriptPattern: "function abc() {" }, + { luaPattern: "function def(", typeScriptPattern: "function def() {" }, + { luaPattern: "return def(", typeScriptPattern: "return def(" }, + { luaPattern: "end", typeScriptPattern: "function def() {" }, + ], + }, + { + code: ` + const enum abc { foo = 2, bar = 4 }; + const xyz = abc.foo; + `, + + assertPatterns: [ + { luaPattern: "xyz", typeScriptPattern: "xyz" }, + { luaPattern: "2", typeScriptPattern: "abc.foo" }, + ], + }, + { + code: ` + // @ts-ignore + import { Foo } from "foo"; + Foo; + `, + + assertPatterns: [ + { luaPattern: 'require("foo")', typeScriptPattern: '"foo"' }, + { luaPattern: "Foo", typeScriptPattern: "Foo" }, + ], + }, + { + code: ` + // @ts-ignore + import * as Foo from "foo"; + Foo; + `, + + assertPatterns: [ + { luaPattern: 'require("foo")', typeScriptPattern: '"foo"' }, + { luaPattern: "Foo", typeScriptPattern: "Foo" }, + ], + }, + { + code: ` + // @ts-ignore + class Bar extends Foo { + constructor() { + super(); + } + } + `, + + assertPatterns: [ + { luaPattern: "Bar = __TS__Class()", typeScriptPattern: "class Bar" }, + { luaPattern: "Bar.name =", typeScriptPattern: "class Bar" }, + { luaPattern: "__TS__ClassExtends(", typeScriptPattern: "extends" }, // find use of function, not import + { luaPattern: "Foo", typeScriptPattern: "Foo" }, + { luaPattern: "function Bar.prototype.____constructor", typeScriptPattern: "constructor" }, + ], + }, + { + code: ` + class Foo { + } + `, + + assertPatterns: [{ luaPattern: "function Foo.prototype.____constructor", typeScriptPattern: "class Foo" }], + }, + { + code: ` + class Foo { + bar = "baz"; + } + `, + + assertPatterns: [{ luaPattern: "function Foo.prototype.____constructor", typeScriptPattern: "class Foo" }], + }, + { + code: ` + declare const arr: string[]; + for (const element of arr) {} + `, + + assertPatterns: [ + { luaPattern: "arr", typeScriptPattern: "arr)" }, + { luaPattern: "element", typeScriptPattern: "element" }, + ], + }, + { + code: ` + declare function getArr(this: void): string[]; + for (const element of getArr()) {} + `, + + assertPatterns: [ + { luaPattern: "for", typeScriptPattern: "for" }, + { luaPattern: "getArr()", typeScriptPattern: "getArr()" }, + { luaPattern: "element", typeScriptPattern: "element" }, + ], + }, + { + code: ` + declare const arr: string[] + for (let i = 0; i < arr.length; ++i) {} + `, + + assertPatterns: [ + { luaPattern: "i = 0", typeScriptPattern: "i = 0" }, + { luaPattern: "i < #arr", typeScriptPattern: "i < arr.length" }, + { luaPattern: "i + 1", typeScriptPattern: "++i" }, + ], + }, +])("Source map has correct mapping (%p)", async ({ code, assertPatterns }) => { + const file = util + .testModule(code) + .ignoreDiagnostics([couldNotResolveRequire.code]) + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); + + const consumer = await new SourceMapConsumer(file.luaSourceMap); + for (const { luaPattern, typeScriptPattern } of assertPatterns) { + const luaPosition = lineAndColumnOf(file.lua, luaPattern); + const mappedPosition = consumer.originalPositionFor(luaPosition); + const typescriptPosition = lineAndColumnOf(code, typeScriptPattern); + + expect(mappedPosition).toMatchObject(typescriptPosition); + } +}); + +test.each([ + { + fileName: "/proj/foo.ts", + config: {}, + expectedSourcePath: "foo.ts", // ts and lua will be emitted to same directory + }, + { + fileName: "/proj/src/foo.ts", + config: { + outDir: "/proj/dst", + }, + expectedSourcePath: "../src/foo.ts", // path from proj/dst outDir to proj/src/foo.ts + }, + { + fileName: "/proj/src/foo.ts", + config: { rootDir: "/proj/src", outDir: "/proj/dst" }, + expectedSourcePath: "../src/foo.ts", // path from proj/dst outDir to proj/src/foo.ts + }, + { + fileName: "/proj/src/sub/foo.ts", + config: { rootDir: "/proj/src", outDir: "/proj/dst" }, + expectedSourcePath: "../../src/sub/foo.ts", // path from proj/dst/sub outDir to proj/src/sub/foo.ts + }, + { + fileName: "/proj/src/sub/main.ts", + config: { rootDir: "/proj/src", outDir: "/proj/dst", sourceRoot: "bin/binsub/binsubsub" }, + expectedSourcePath: "../../src/sub/main.ts", // path from proj/dst/sub outDir to proj/src/sub/foo.ts + fullSource: "bin/src/sub/main.ts", + }, +])("Source map has correct sources (%p)", async ({ fileName, config, fullSource, expectedSourcePath }) => { + const file = util.testModule` + const foo = "foo" + ` + .setOptions(config) + .setMainFileName(fileName) + .getMainLuaFileResult(); + + const sourceMap = JSON.parse(file.luaSourceMap); + expect(sourceMap.sources).toHaveLength(1); + expect(sourceMap.sources[0]).toBe(expectedSourcePath); + + const consumer = await new SourceMapConsumer(file.luaSourceMap); + expect(consumer.sources).toHaveLength(1); + expect(consumer.sources[0]).toBe(fullSource ?? expectedSourcePath); +}); + +test.each([ + { configSourceRoot: undefined, mapSourceRoot: "" }, + { configSourceRoot: "src", mapSourceRoot: "src/" }, + { configSourceRoot: "src/", mapSourceRoot: "src/" }, + { configSourceRoot: "src\\", mapSourceRoot: "src/" }, +])("Source map has correct source root (%p)", ({ configSourceRoot, mapSourceRoot }) => { + const file = util.testModule` + const foo = "foo" + ` + .setOptions({ sourceMap: true, sourceRoot: configSourceRoot }) + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); + + const sourceMap = JSON.parse(file.luaSourceMap); + expect(sourceMap.sourceRoot).toBe(mapSourceRoot); +}); + +test.each([ + { code: 'const type = "foobar";', name: "type" }, + { code: 'const and = "foobar";', name: "and" }, + { code: 'const $$$ = "foobar";', name: "$$$" }, + { code: "const foo = { bar() { this; } };", name: "this" }, + { code: "function foo($$$: unknown) {}", name: "$$$" }, + { code: "class $$$ {}", name: "$$$" }, + { code: 'namespace $$$ { const foo = "bar"; }', name: "$$$" }, +])("Source map has correct name mappings (%p)", async ({ code, name }) => { + const file = util.testModule(code).expectToHaveNoDiagnostics().getMainLuaFileResult(); + + const consumer = await new SourceMapConsumer(file.luaSourceMap); + const typescriptPosition = lineAndColumnOf(code, name); + let mappedName: string | undefined; + consumer.eachMapping(mapping => { + if (mapping.originalLine === typescriptPosition.line && mapping.originalColumn === typescriptPosition.column) { + mappedName = mapping.name; + } + }); + + expect(mappedName).toBe(name); +}); + +test("sourceMapTraceback saves sourcemap in _G", () => { + const code = ` + function abc() { + return "foo"; + } + + return (globalThis as any).__TS__sourcemap; + `; + + const builder = util + .testFunction(code) + .setOptions({ sourceMapTraceback: true, luaLibImport: tstl.LuaLibImportKind.Inline }); + + const sourceMap = builder.getLuaExecutionResult(); + const transpiledLua = builder.getMainLuaCodeChunk(); + + expect(sourceMap).toEqual(expect.any(Object)); + const sourceMapFiles = Object.keys(sourceMap); + expect(sourceMapFiles).toHaveLength(1); + const mainSourceMap = sourceMap[sourceMapFiles[0]]; + + const assertPatterns = [ + { luaPattern: "function abc(", typeScriptPattern: "function abc() {" }, + { luaPattern: 'return "foo"', typeScriptPattern: 'return "foo"' }, + ]; + + for (const { luaPattern, typeScriptPattern } of assertPatterns) { + const luaPosition = lineAndColumnOf(transpiledLua, luaPattern); + const mappedLine = mainSourceMap[luaPosition.line.toString()]; + + const typescriptPosition = lineAndColumnOf(code, typeScriptPattern); + expect(mappedLine).toEqual(typescriptPosition.line); + } +}); + +test("sourceMapTraceback gives traceback", () => { + const builder = util.testFunction` + return (debug.traceback as (this: void)=>string)(); + `.setOptions({ sourceMapTraceback: true }); + + const traceback = builder.getLuaExecutionResult(); + expect(traceback).toEqual(expect.any(String)); +}); + +test("inline sourceMapTraceback gives traceback", () => { + const builder = util.testFunction` + return (debug.traceback as (this: void)=>string)(); + `.setOptions({ sourceMapTraceback: true, luaLibImport: tstl.LuaLibImportKind.Inline }); + + const traceback = builder.getLuaExecutionResult(); + expect(traceback).toEqual(expect.any(String)); +}); + +test("Inline sourcemaps", () => { + const code = ` + function abc() { + return def(); + } + + function def() { + return "foo"; + } + + return abc(); + `; + + const file = util + .testFunction(code) + .setOptions({ inlineSourceMap: true }) // We can't disable 'sourceMap' option because it's used for comparison + .disableSemanticCheck() // TS5053: Option 'sourceMap' cannot be specified with option 'inlineSourceMap'. + .expectToMatchJsResult() + .getMainLuaFileResult(); + + const [, inlineSourceMapMatch] = + file.lua.match(/--# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/) ?? []; + const inlineSourceMap = Buffer.from(inlineSourceMapMatch, "base64").toString(); + expect(inlineSourceMap).toBe(file.luaSourceMap); +}); + +test("loadstring sourceMapTraceback gives traceback", () => { + const loadStrCode = transpileString( + `function bar() { + const trace = (debug.traceback as (this: void)=>string)(); + return trace; + } + return bar();`, + { sourceMapTraceback: true, luaTarget: LuaTarget.Lua51 } + ).file?.lua; + + const builder = util.testModule` + const luaCode = \`${loadStrCode}\`; + return loadstring(luaCode, "foo.lua")(); + ` + .setTsHeader("declare function loadstring(this: void, string: string, chunkname: string): () => unknown") + .setOptions({ sourceMapTraceback: true, luaTarget: LuaTarget.Lua51 }); + + const traceback = builder.getLuaExecutionResult(); + expect(traceback).toContain("foo.ts"); +}); diff --git a/test/unit/printer/utils.ts b/test/unit/printer/utils.ts new file mode 100644 index 000000000..a9f58b44f --- /dev/null +++ b/test/unit/printer/utils.ts @@ -0,0 +1,25 @@ +// Helper functions + +import { Position } from "source-map"; + +export function lineAndColumnOf(text: string, pattern: string): Position { + const pos = text.indexOf(pattern); + if (pos === -1) { + return { line: -1, column: -1 }; + } + + const lineLengths = text.split("\n").map(s => s.length); + + let totalPos = 0; + for (let line = 1; line <= lineLengths.length; line++) { + // Add + 1 for the removed \n + const lineLength = lineLengths[line - 1] + 1; + if (pos < totalPos + lineLength) { + return { line, column: pos - totalPos }; + } + + totalPos += lineLengths[line - 1] + 1; + } + + return { line: -1, column: -1 }; +} diff --git a/test/unit/require.spec.ts b/test/unit/require.spec.ts deleted file mode 100644 index 233237c5d..000000000 --- a/test/unit/require.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Expect, FocusTest, Test, TestCase } from "alsatian"; - -import * as util from "../src/util"; -import * as path from "path"; -import { CompilerOptions } from "../../src/CompilerOptions"; - -export class RequireTests { - - @TestCase("file.ts", "./folder/Module", "folder.Module", { rootDir: "." }, false) - @TestCase("file.ts", "./folder/Module", "folder.Module", { rootDir: "./" }, false) - @TestCase("src/file.ts", "./folder/Module", "src.folder.Module", { rootDir: "." }, false) - @TestCase("file.ts", "folder/Module", "folder.Module", { rootDir: ".", baseUrl: "." }, false) - @TestCase("file.ts", "folder/Module", "folder.Module", { rootDir: "./", baseUrl: "." }, false) - @TestCase("src/file.ts", "./folder/Module", "folder.Module", { rootDir: "src" }, false) - @TestCase("src/file.ts", "./folder/Module", "folder.Module", { rootDir: "./src" }, false) - @TestCase("file.ts", "../Module", "", { rootDir: "./src" }, true) - @TestCase("src/dir/file.ts", "../Module", "Module", { rootDir: "./src" }, false) - @TestCase("src/dir/dir/file.ts", "../../dir/Module", "dir.Module", { rootDir: "./src" }, false) - @Test("require paths root from --baseUrl or --rootDir") - public testRequirePath( - filePath: string, - usedPath: string, - expectedPath: string, - options: CompilerOptions, - throwsError: boolean): void { - const regex = /require\("(.*?)"\)/; // This regex extracts `hello` from require("hello") - if (throwsError) { - Expect(() => util.transpileString(`import * from "${usedPath}";`, options, true, filePath)).toThrow(); - } else { - const lua = util.transpileString(`import * from "${usedPath}";`, options, true, filePath); - const match = regex.exec(lua); - Expect(match[1]).toBe(expectedPath); - } - } - -} \ No newline at end of file diff --git a/test/unit/spread.spec.ts b/test/unit/spread.spec.ts new file mode 100644 index 000000000..53898aae5 --- /dev/null +++ b/test/unit/spread.spec.ts @@ -0,0 +1,579 @@ +import * as tstl from "../../src"; +import * as util from "../util"; +import { formatCode } from "../util"; + +// TODO: Make some utils for testing other targets +const expectUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toMatch(/[^.]unpack\(/); +const expectTableUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("table.unpack"); +const expectLualibUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("__TS__Unpack"); + +const arrayLiteralCases = [ + "1, 2, ...[3, 4, 5]", + "...[1, 2], 3, 4, 5", + "1, ...[[2]], 3", + "...[1, 2, 3], 4, ...[5, 6]", + "1, 2, ...[3, 4], ...[5, 6]", +]; + +describe.each(["function call", "array literal"] as const)("in %s", kind => { + const factory = (code: string) => (kind === "function call" ? `((...args: any[]) => args)(${code})` : `[${code}]`); + + test.each(arrayLiteralCases)("of array literal (%p)", expression => { + util.testExpression(factory(expression)).expectToMatchJsResult(); + }); + + test.each(arrayLiteralCases)("of multi return call (%p)", expression => { + util.testFunction` + function tuple(...args: any[]) { + return $multi(...args); + } + + return ${factory(`...tuple(${expression})`)}; + ` + .withLanguageExtensions() + .expectToMatchJsResult(); + }); + + test("of multiple string literals", () => { + util.testExpression(factory('..."spread", ..."string"')).expectToMatchJsResult(); + }); + + test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => { + util.testExpression(factory(`...${formatCode(str)}`)).expectToMatchJsResult(); + }); + + test("of iterable", () => { + util.testFunction` + const it = { + i: -1, + [Symbol.iterator]() { + return this; + }, + next() { + ++this.i; + return { + value: 2 ** this.i, + done: this.i == 9, + } + } + }; + + return ${factory("...it")}; + `.expectToMatchJsResult(); + }); +}); + +describe("in function call", () => { + util.testEachVersion( + undefined, + () => util.testFunction` + function foo(a: number, b: number, ...rest: number[]) { + return { a, b, rest } + } + const array = [0, 1, 2, 3] as const; + return foo(...array); + `, + { + [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), + [tstl.LuaTarget.Lua50]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), + [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(), + } + ); +}); + +describe("in array literal", () => { + util.testEachVersion(undefined, () => util.testExpression`[...[0, 1, 2]]`, { + [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), + [tstl.LuaTarget.Lua50]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), + [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", () => { + util.testFunction` + const array = [1, 2, ...[3], 5, , 6]; + return { a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); +}); + +describe("in object literal", () => { + test.each([ + "{ x: false, ...{ x: true, y: true } }", + "{ ...{ x: true, y: true } }", + "{ ...{ x: true }, ...{ y: true, z: true } }", + "{ ...{ x: false }, x: true }", + "{ ...{ x: false }, x: false, ...{ x: true } }", + ])("of object literal (%p)", expression => { + util.testExpression(expression).expectToMatchJsResult(); + }); + + test("of object reference", () => { + util.testFunction` + const object = { x: 0, y: 1 }; + const result = { ...object, z: 2 }; + return { object, result }; + `.expectToMatchJsResult(); + }); + + test.each([ + ["literal", "const object = { ...[0, 1, 2] };"], + ["reference", "const array = [0, 1, 2]; const object = { ...array };"], + ])("of array %p", (_name, expressionToCreateObject) => { + util.testFunction` + ${expressionToCreateObject} + return { "0": object[0], "1": object[1], "2": object[2] }; + `.expectToMatchJsResult(); + }); +}); + +describe("vararg spread optimization", () => { + util.testEachVersion( + "basic use", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + return pick(...args); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "body-less arrow function", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + const test = (...args: string[]) => pick(...args); + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "if statement", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + if (true) { + return pick(...args); + } + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "loop statement", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + do { + return pick(...args); + } while (false); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "block statement", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + let result: string; + { + result = pick(...args); + } + return result; + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "finally clause", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + try { + throw "foobar"; + } catch { + } finally { + return pick(...args); + } + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + test("$multi", () => { + util.testFunction` + function multi(...args: string[]) { + return $multi(...args); + } + function test(...args: string[]) { + return multi(...args)[1]; + } + return test("a" ,"b", "c"); + ` + .withLanguageExtensions() + .expectLuaToMatchSnapshot() + .expectToEqual("b"); + }); + + util.testEachVersion( + "curry", + () => util.testFunction` + function test(fn: (...args: A) => void, ...args: A) { + return fn(...args); + } + return test((arg: string) => arg, "foobar");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "curry with indirect type", + () => util.testFunction` + function test(obj: {fn: (...args: A) => void}, ...args: A) { + const fn = obj.fn; + return fn(...args); + } + return test({fn: (arg: string) => arg}, "foobar");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "function type declared inside scope", + () => util.testFunction` + function test(...args: A) { + const fn: (...args: A) => A[0] = (...args) => args[0]; + return fn(...args); + } + test("foobar");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "With cast", + () => util.testFunction` + function pick(...args: any[]) { return args[1]; } + function testany>(...args: Parameters) { + return pick(...(args as any[])); + } + return test<(...args: string[])=>void>("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); +}); + +describe("vararg spread de-optimization", () => { + util.testEachVersion( + "array modification", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + args[1] = "foobar"; + return pick(...args); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "array modification in hoisted function", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + hoisted(); + const result = pick(...args); + function hoisted() { args[1] = "foobar"; } + return result; + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "array modification in secondary hoisted function", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + function triggersHoisted() { hoisted(); } + triggersHoisted(); + const result = pick(...args); + function hoisted() { args[1] = "foobar"; } + return result; + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); +}); + +describe("vararg spread in IIFE", () => { + util.testEachVersion( + "comma operator", + () => util.testFunction` + function dummy() { return "foobar"; } + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + return (dummy(), pick(...args)); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "assignment expression", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + let x: string; + return (x = pick(...args)); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "destructured assignment expression", + () => util.testFunction` + function pick(...args: string[]) { return [args[1]]; } + function test(...args: string[]) { + let x: string; + return ([x] = pick(...args)); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "property-access assignment expression", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + let x: {val?: string} = {}; + return (x.val = pick(...args)); + } + return test("a", "b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "binary compound assignment", + () => util.testFunction` + function pick(...args: number[]) { return args[1]; } + function test(...args: number[]) { + let x = 1; + return x += pick(...args); + } + return test(1, 2, 3);`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "postfix unary compound assignment", + () => util.testFunction` + function pick(...args: number[]) { return args[1]; } + function test(...args: number[]) { + let x = [7, 8, 9]; + return x[pick(...args)]++; + } + return test(1, 2, 3);`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "prefix unary compound assignment", + () => util.testFunction` + function pick(...args: number[]) { return args[1]; } + function test(...args: number[]) { + let x = [7, 8, 9]; + return ++x[pick(...args)]; + } + return test(1, 2, 3);`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "try clause", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + try { + return pick(...args) + } catch {} + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "catch clause", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + try { + throw "foobar"; + } catch { + return pick(...args) + } + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "class expression", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + function test(...args: string[]) { + const fooClass = class Foo { foo = pick(...args); }; + return new fooClass().foo; + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "self-referencing function expression", + () => util.testFunction` + function pick(...args: string[]) { return args[1]; } + const test = function testName(...args: string[]) { + return \`\${typeof testName}:\${pick(...args)}\`; + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "method indirect access (access args)", + () => util.testFunction` + const obj = { $method: () => obj.arg, arg: "foobar" }; + function getObj(...args: string[]) { obj.arg = args[1]; return obj; } + function test(...args: string[]) { + return getObj(...args).$method(); + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "method indirect access (method args)", + () => util.testFunction` + const obj = { $pick: (...args: string[]) => args[1] }; + function getObj() { return obj; } + function test(...args: string[]) { + return getObj().$pick(...args); + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); + + util.testEachVersion( + "tagged template method indirect access", + () => util.testFunction` + const obj = { $tag: (t: TemplateStringsArray, ...args: string[]) => args[1] }; + function getObj() { return obj; } + function pick(...args: string[]): string { return args[1]; } + function test(...args: string[]) { + return getObj().$tag\`FOO\${pick(...args)}BAR\`; + } + return test("a" ,"b", "c");`, + util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) + ); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1244 +test.each(["pairs", "ipairs"])("can spread %s (#1244)", func => { + util.testFunction` + const arr = ["a", "b", "c"]; + return [...${func}(arr)]; + ` + .withLanguageExtensions() + .setTsHeader( + ` + declare function ipairs(this: void, t: T): LuaIterable]>>; + declare function pairs(this: void, t: T): LuaIterable]>>; + ` + ) + .expectToEqual([ + [1, "a"], + [2, "b"], + [3, "c"], + ]); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1244 +test.each(["LuaTable", "LuaMap"])("can spread %s with pairs (#1244)", type => { + const result: Array<[string, string]> = util.testFunction` + const tbl = new ${type}(); + tbl.set("foo", "bar"); + tbl.set("fizz", "buzz"); + return [...pairs(tbl)]; + ` + .withLanguageExtensions() + .setTsHeader( + "declare function pairs(this: void, t: T): LuaIterable]>>;" + ) + .getLuaExecutionResult(); + + // We don't know the order so match like this + expect(result).toHaveLength(2); + expect(result).toContainEqual(["foo", "bar"]); + expect(result).toContainEqual(["fizz", "buzz"]); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1384 +test.each(["LuaTable", "LuaMap"])("can spread %s (#1384)", type => { + const result: Array<[string, string]> = util.testFunction` + const tbl = new ${type}(); + tbl.set("foo", "bar"); + tbl.set("fizz", "buzz"); + return [...tbl]; + ` + .withLanguageExtensions() + .getLuaExecutionResult(); + + // We don't know the order so match like this + expect(result).toHaveLength(2); + expect(result).toContainEqual(["foo", "bar"]); + expect(result).toContainEqual(["fizz", "buzz"]); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1384 +test("can spread LuaSet (#1384)", () => { + const result: Array<[string, string]> = util.testFunction` + const tbl = new LuaSet(); + tbl.add("foo"); + tbl.add("bar"); + return [...tbl]; + ` + .withLanguageExtensions() + .getLuaExecutionResult(); + + // We don't know the order so match like this + expect(result).toHaveLength(2); + expect(result).toContain("foo"); + expect(result).toContain("bar"); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1426 +test.each([true, false])("spread a decorator or array union type (#1426)", choice => { + util.testFunction` + function* things() { + yield "a"; + yield "b" + } + const c: boolean = ${util.formatCode(choice)}; + return [...(c ? things() : ["c", "d"])]; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/spreadElement.spec.ts b/test/unit/spreadElement.spec.ts deleted file mode 100644 index 35249ce15..000000000 --- a/test/unit/spreadElement.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import { LuaTarget, LuaLibImportKind } from "../../src/CompilerOptions"; -import * as util from "../src/util"; - -export class SpreadElementTest { - - @TestCase([]) - @TestCase([1, 2, 3]) - @TestCase([1, "test", 3]) - @Test("Spread Element Push") - public spreadElementPush(inp: any[]): void - { - const result = util.transpileAndExecute(`return JSONStringify([].push(...${JSON.stringify(inp)}));`); - Expect(result).toBe([].push(...inp)); - } - - @Test("Spread Element Lua 5.1") - public spreadElement51(): void - { - // Cant test functional because our VM doesn't run on 5.1 - const lua = util.transpileString(`[].push(...${JSON.stringify([1, 2, 3])});`, {luaTarget: LuaTarget.Lua51}); - Expect(lua).toBe("__TS__ArrayPush({}, unpack({1, 2, 3}));"); - } - - @Test("Spread Element Lua 5.2") - public spreadElement52(): void - { - const options = {luaTarget: LuaTarget.Lua52, luaLibImport: LuaLibImportKind.None}; - const lua = util.transpileString(`[...[0, 1, 2]]`, options); - Expect(lua).toBe("{table.unpack({0, 1, 2})};"); - } - - @Test("Spread Element Lua 5.3") - public spreadElement53(): void - { - const options = {luaTarget: LuaTarget.Lua53, luaLibImport: LuaLibImportKind.None}; - const lua = util.transpileString(`[...[0, 1, 2]]`, options); - Expect(lua).toBe("{table.unpack({0, 1, 2})};"); - } - - @Test("Spread Element Lua JIT") - public spreadElementJIT(): void - { - const options = {luaTarget: "JiT" as LuaTarget, luaLibImport: LuaLibImportKind.None}; - const lua = util.transpileString(`[...[0, 1, 2]]`, options); - Expect(lua).toBe("{unpack({0, 1, 2})};"); - } -} diff --git a/test/unit/statements.spec.ts b/test/unit/statements.spec.ts new file mode 100644 index 000000000..d653a8885 --- /dev/null +++ b/test/unit/statements.spec.ts @@ -0,0 +1,19 @@ +import * as util from "../util"; +import { unsupportedNodeKind } from "../../src/transformation/utils/diagnostics"; + +test("Block statement", () => { + util.testFunction` + let a = 4; + { let a = 42; } + return a; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/860 +test("Unsupported node adds diagnostic", () => { + util.testModule` + const a = () => { + foo: "bar" + }; + `.expectDiagnosticsToMatchSnapshot([unsupportedNodeKind.code]); +}); diff --git a/test/unit/string.spec.ts b/test/unit/string.spec.ts deleted file mode 100644 index b3326ec78..000000000 --- a/test/unit/string.spec.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { TranspileError } from "../../src/TranspileError"; -import * as util from "../src/util"; - -export class StringTests -{ - @Test("Unsuported string function") - public stringUnsuportedFunction(): void { - // Assert - Expect(() => { - util.transpileString( - `return "test".testThisIsNoMember()` - ); - }).toThrowError(TranspileError, "Unsupported property on string: testThisIsNoMember"); - } - - @TestCase([]) - @TestCase([65]) - @TestCase([65, 66]) - @TestCase([65, 66, 67]) - @Test("String.fromCharCode") - public stringFromCharcode(inp: number[]): void - { - const result = util.transpileAndExecute( - `return String.fromCharCode(${inp.toString()})` - ); - - // Assert - Expect(result).toBe(String.fromCharCode(...inp)); - } - - @TestCase(12, 23, 43) - @TestCase("test", "hello", "bye") - @TestCase("test", 42, "bye") - @TestCase("test", 42, 12) - @Test("Template Strings") - public templateStrings(a: any, b: any, c: any): void { - // Transpile - const a1 = typeof(a) === "string" ? "'" + a + "'" : a; - const b1 = typeof(b) === "string" ? "'" + b + "'" : b; - const c1 = typeof(c) === "string" ? "'" + c + "'" : c; - - const result = util.transpileAndExecute( - "let a = " + a1 + "; let b = " + b1 + "; let c = " + c1 + "; return `${a} ${b} test ${c}`;" - ); - - // Assert - Expect(result).toBe(`${a} ${b} test ${c}`); - } - - @TestCase("hello test", "", "") - @TestCase("hello test", " ", "") - @TestCase("hello test", "hello", "") - @TestCase("hello test", "test", "") - @TestCase("hello test", "test", "world") - @Test("string.replace") - public replace(inp: string, searchValue: string, replaceValue: string): void - { - const result = util.transpileAndExecute( - `return "${inp}".replace("${searchValue}", "${replaceValue}");` - ); - - // Assert - Expect(result).toBe(inp.replace(searchValue, replaceValue)); - } - - @TestCase(["", ""], "") - @TestCase(["hello", "test"], "hellotest") - @TestCase(["hello", "test", "bye"], "hellotestbye") - @TestCase(["hello", 42], "hello42") - @TestCase([42, "hello"], "42hello") - @Test("string.concat[+]") - public concat(inp: any[], expected: string): void { - const concatStr = inp.map(elem => typeof(elem) === "string" ? `"${elem}"` : elem).join(" + "); - - // Transpile/Execute - const result = util.transpileAndExecute( - `return ${concatStr}` - ); - - // Assert - Expect(result).toBe(expected); - } - @TestCase("", ["", ""]) - @TestCase("hello", ["test"]) - @TestCase("hello", []) - @TestCase("hello", ["test", "bye"]) - @Test("string.concatFct") - public concatFct(str: string, param: string[]): void { - const paramStr = param.map(elem => `"${elem}"`).join(", "); - const result = util.transpileAndExecute( - `return "${str}".concat(${paramStr})` - ); - // Assert - Expect(result).toBe(str.concat(...param)); - } - @TestCase("hello test", "") - @TestCase("hello test", "t") - @TestCase("hello test", "h") - @TestCase("hello test", "invalid") - @Test("string.indexOf") - public indexOf(inp: string, searchValue: string): void - { - const result = util.transpileAndExecute(`return "${inp}".indexOf("${searchValue}")`); - - // Assert - Expect(result).toBe(inp.indexOf(searchValue)); - } - - @TestCase("hello test", "t", 5) - @TestCase("hello test", "t", 6) - @TestCase("hello test", "t", 7) - @TestCase("hello test", "h", 4) - @Test("string.indexOf with offset") - public indexOfOffset(inp: string, searchValue: string, offset: number): void - { - const result = util.transpileAndExecute( - `return "${inp}".indexOf("${searchValue}", ${offset})` - ); - - // Assert - Expect(result).toBe(inp.indexOf(searchValue, offset)); - } - - @TestCase("hello test", "t", 4, 3) - @TestCase("hello test", "h", 3, 4) - @Test("string.indexOf with offset expression") - public indexOfOffsetWithExpression(inp: string, searchValue: string, x: number, y: number): void - { - const result = util.transpileAndExecute( - `return "${inp}".indexOf("${searchValue}", 2 > 1 && ${x} || ${y})` - ); - - // Assert - Expect(result).toBe(inp.indexOf(searchValue, x)); - } - @TestCase("hello test") - @TestCase("hello test", 0) - @TestCase("hello test", 1) - @TestCase("hello test", 1, 2) - @TestCase("hello test", 1, 5) - @Test("string.slice") - public slice(inp: string, start?: number, end?: number): void - { - // Transpile/Execute - const paramStr = start? (end ? `${start}, ${end}` : `${start}`):''; - const result = util.transpileAndExecute( - `return "${inp}".slice(${paramStr})` - ); - - // Assert - Expect(result).toBe(inp.slice(start, end)); - } - @TestCase("hello test", 0) - @TestCase("hello test", 1) - @TestCase("hello test", 1, 2) - @TestCase("hello test", 1, 5) - @Test("string.substring") - public substring(inp: string, start: number, end?: number): void - { - // Transpile/Execute - const paramStr = end ? `${start}, ${end}` : `${start}`; - const result = util.transpileAndExecute( - `return "${inp}".substring(${paramStr})` - ); - - // Assert - Expect(result).toBe(inp.substring(start, end)); - } - - @TestCase("hello test", 1, 0) - @TestCase("hello test", 3, 0, 5) - @Test("string.substring with expression") - public substringWithExpression(inp: string, start: number, ignored: number, end?: number): void - { - // Transpile/Execute - const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); - const result = util.transpileAndExecute( - `return "${inp}".substring(${paramStr})` - ); - - // Assert - Expect(result).toBe(inp.substring(start, end)); - } - - @TestCase("hello test", 0) - @TestCase("hello test", 1) - @TestCase("hello test", 1, 2) - @TestCase("hello test", 1, 5) - @Test("string.substr") - public substr(inp: string, start: number, end?: number): void - { - // Transpile/Execute - const paramStr = end ? `${start}, ${end}` : `${start}`; - const result = util.transpileAndExecute( - `return "${inp}".substr(${paramStr})` - ); - - // Assert - Expect(result).toBe(inp.substr(start, end)); - } - - @TestCase("hello test", 1, 0) - @TestCase("hello test", 3, 0, 2) - @Test("string.substr with expression") - public substrWithExpression(inp: string, start: number, ignored: number, end?: number): void - { - // Transpile/Execute - const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); - const result = util.transpileAndExecute( - `return "${inp}".substr(${paramStr})` - ); - - // Assert - Expect(result).toBe(inp.substr(start, end)); - } - - @TestCase("", 0) - @TestCase("h", 1) - @TestCase("hello", 5) - @Test("string.length") - public length(inp: string, expected: number): void - { - const result = util.transpileAndExecute( - `return "${inp}".length` - ); - - // Assert - Expect(result).toBe(inp.length); - } - - @TestCase("hello TEST") - @Test("string.toLowerCase") - public toLowerCase(inp: string): void - { - const result = util.transpileAndExecute( - `return "${inp}".toLowerCase()` - ); - - // Assert - Expect(result).toBe(inp.toLowerCase()); - } - - @TestCase("hello test") - @Test("string.toUpperCase") - public toUpperCase(inp: string): void - { - const result = util.transpileAndExecute( - `return "${inp}".toUpperCase()` - ); - - // Assert - Expect(result).toBe(inp.toUpperCase()); - } - - @TestCase("hello test", "") - @TestCase("hello test", " ") - @TestCase("hello test", "h") - @TestCase("hello test", "t") - @TestCase("hello test", "l") - @TestCase("hello test", "invalid") - @TestCase("hello test", "hello test") - @Test("string.split") - public split(inp: string, separator: string): void - { - const result = util.transpileAndExecute( - `return JSONStringify("${inp}".split("${separator}"))` - ); - - // Assert - Expect(result).toBe(JSON.stringify(inp.split(separator))); - } - - @TestCase("hello test", 1) - @TestCase("hello test", 2) - @TestCase("hello test", 3) - @TestCase("hello test", 99) - @Test("string.charAt") - public charAt(inp: string, index: number): void - { - const result = util.transpileAndExecute( - `return "${inp}".charAt(${index})` - ); - - // Assert - Expect(result).toBe(inp.charAt(index)); - } - - @TestCase("hello test", 1) - @TestCase("hello test", 2) - @TestCase("hello test", 3) - @Test("string.charCodeAt") - public charCodeAt(inp: string, index: number): void - { - const result = util.transpileAndExecute( - `return "${inp}".charCodeAt(${index})` - ); - - // Assert - Expect(result).toBe(inp.charCodeAt(index)); - } - - @TestCase("hello test", 1, 0) - @TestCase("hello test", 1, 2) - @TestCase("hello test", 3, 2) - @TestCase("hello test", 3, 99) - @Test("string.charAt with expression") - public charAtWithExpression(inp: string, index: number, ignored: number): void - { - const result = util.transpileAndExecute( - `return "${inp}".charAt(2 > 1 && ${index} || ${ignored})` - ); - - // Assert - Expect(result).toBe(inp.charAt(index)); - } - - @TestCase("abcd", 3) - @TestCase("abcde", 3) - @TestCase("abcde", 0) - @TestCase("a", 0) - @Test("string index") - public index(input: string, index: number): void - { - const result = util.transpileAndExecute(`return "${input}"[${index}];`); - - Expect(result).toBe(input[index]); - } -} diff --git a/test/unit/switch.spec.ts b/test/unit/switch.spec.ts new file mode 100644 index 000000000..4fb72c40c --- /dev/null +++ b/test/unit/switch.spec.ts @@ -0,0 +1,664 @@ +import * as util from "../util"; + +test.each([0, 1, 2, 3])("switch (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp}) { + case 0: + result = 0; + break; + case 1: + result = 1; + break; + case 2: + result = 2; + break; + } + return result; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("switchdefault (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp}) { + case 0: + result = 0; + break; + case 1: + result = 1; + break; + case 2: + result = 2; + break; + default: + result = -2; + break; + } + return result; + `.expectToMatchJsResult(); +}); + +test.each([0, 0, 2, 3, 4, 5, 7])("switchfallthrough (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp}) { + case 0: + result = 0; + case 1: + result = 1; + break; + case 2: + result = 2; + case 3: + case 4: + result = 4; + break; + case 5: + result = 5; + case 6: + result += 10; + break; + case 7: + result = 7; + default: + result = -2; + break; + } + + return result; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("nestedSwitch (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp} as number) { + case 0: + result = 0; + break; + case 1: + switch(${inp} as number) { + case 0: + result = 0; + break; + case 1: + result = 1; + break; + default: + result = -3; + break; + } + break; + case 2: + result = 2; + break; + default: + result = -2; + break; + } + return result; + `.expectToMatchJsResult(); +}); + +test("switch cases scope", () => { + util.testFunction` + switch (0 as number) { + case 0: + let foo: number | undefined = 1; + case 1: + foo = 2; + case 2: + return foo; + } + `.expectToMatchJsResult(); +}); + +test("variable in nested scope does not interfere with case scope", () => { + util.testFunction` + let foo: number = 0; + switch (foo) { + case 0: { + let foo = 1; + } + + case 1: + return foo; + } + `.expectToMatchJsResult(); +}); + +test("switch using variable re-declared in cases", () => { + util.testFunction` + let foo: number = 0; + switch (foo) { + case 0: + let foo = true; + case 1: + return foo; + } + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2])("switch with block statement scope (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp}) { + case 0: { + let x = 0; + result = 0; + break; + } + case 1: { + let x = 1; + result = x; + } + case 2: { + let x = 2; + result = x; + break; + } + } + return result; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("switchReturn (%p)", inp => { + util.testFunction` + switch (${inp}) { + case 0: + return 0; + break; + case 1: + return 1; + case 2: + return 2; + break; + } + + return -1; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("switchWithBrackets (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp}) { + case 0: { + result = 0; + break; + } + case 1: { + result = 1; + break; + } + case 2: { + result = 2; + break; + } + } + return result; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3, 4])("switchWithBracketsBreakInConditional (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp}) { + case 0: { + result = 0; + break; + } + case 1: { + result = 1; + + if (result == 1) break; + } + case 2: { + result = 2; + + if (result != 2) break; + } + case 3: { + result = 3; + break; + } + } + return result; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3])("switchWithBracketsBreakInInternalLoop (%p)", inp => { + util.testFunction` + let result: number = -1; + + switch (${inp} as number) { + case 0: { + result = 0; + + for (let i = 0; i < 5; i++) { + result++; + + if (i >= 2) { + break; + } + } + } + case 1: { + result++; + break; + } + case 2: { + result = 2; + break; + } + } + return result; + `.expectToMatchJsResult(); +}); + +test("switch executes only one clause", () => { + util.testFunction` + let result: number = -1; + switch (2 as number) { + case 0: { + result = 200; + break; + } + + case 1: { + result = 100; + break; + } + + case 2: { + result = 1; + break; + } + } + return result; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/967 +test("switch default case not last - first", () => { + util.testFunction` + switch (3 as number) { + default: + return "wrong"; + case 3: + return "right"; + } + `.expectToMatchJsResult(); +}); + +test("switch default case not last - second", () => { + util.testFunction` + switch (3 as number) { + case 4: + return "also wrong"; + default: + return "wrong"; + case 3: + return "right"; + } + `.expectToMatchJsResult(); +}); + +test("switch default case only", () => { + util.testFunction` + let out = 0; + switch (4 as number) { + default: + out = 1 + } + return out; + `.expectToMatchJsResult(); +}); + +test("switch fallthrough enters default", () => { + util.testFunction` + const out = []; + switch (3 as number) { + case 3: + out.push("3"); + default: + out.push("default"); + } + return out; + `.expectToMatchJsResult(); +}); + +test("switch fallthrough does not enter earlier default", () => { + util.testFunction` + const out = []; + switch (3 as number) { + default: + out.push("default"); + case 3: + out.push("3"); + } + return out; + `.expectToMatchJsResult(); +}); + +test("switch fallthrough stops after default", () => { + util.testFunction` + const out = []; + switch (4 as number) { + default: + out.push("default"); + case 3: + out.push("3"); + } + return out; + `.expectToMatchJsResult(); +}); + +test.each([0, 1])("switch empty fallthrough to default (%p)", inp => { + util.testFunction` + const out = []; + switch (${inp} as number) { + case 1: + default: + out.push("default"); + + } + return out; + ` + .expectLuaToMatchSnapshot() + .expectToMatchJsResult(); +}); + +test("switch does not pollute parent scope", () => { + util.testFunction` + let x: number = 0; + let y = 1; + switch (x) { + case 0: + let y = 2; + } + return y; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3, 4])("switch handles side-effects (%p)", inp => { + util.testFunction` + const out = []; + + let y = 0; + function foo() { + return y++; + } + + let x = ${inp} as number; + switch (x) { + case foo(): + out.push(1); + case foo(): + out.push(2); + case foo(): + out.push(3); + default: + out.push("default"); + case foo(): + } + + out.push(y); + return out; + `.expectToMatchJsResult(); +}); + +test.each([1, 2])("switch handles side-effects with empty fallthrough (%p)", inp => { + util.testFunction` + const out = []; + + let y = 0; + function foo() { + return y++; + } + + let x = 0 as number; + switch (x) { + // empty fallthrough 1 or many times + ${new Array(inp).fill("case foo():").join("\n")} + default: + out.push("default"); + + } + + out.push(y); + return out; + `.expectToMatchJsResult(); +}); + +test.each([1, 2])("switch handles side-effects with empty fallthrough (preceding clause) (%p)", inp => { + util.testFunction` + const out = []; + + let y = 0; + function foo() { + return y++; + } + + let x = 0 as number; + switch (x) { + case 1: + out.push(1); + // empty fallthrough 1 or many times + ${new Array(inp).fill("case foo():").join("\n")} + default: + out.push("default"); + + } + + out.push(y); + return out; + `.expectToMatchJsResult(); +}); + +test.each([0, 1, 2, 3, 4])("switch handles async side-effects (%p)", inp => { + util.testFunction` + (async () => { + const out = []; + + let y = 0; + async function foo() { + return new Promise((resolve) => y++ && resolve(0)); + } + + let x = ${inp} as number; + switch (x) { + case await foo(): + out.push(1); + case await foo(): + out.push(2); + case await foo(): + out.push(3); + default: + out.push("default"); + case await foo(): + } + + out.push(y); + return out; + })(); + `.expectToMatchJsResult(); +}); + +const optimalOutput = (c: number) => util.testFunction` + let x: number = 0; + const out = []; + switch (${c} as number) { + case 0: + case 1: + case 2: + out.push("0,1,2"); + break; + default: + x++; + out.push("default = " + x); + case 3: { + out.push("3"); + break; + } + case 4: + } + out.push(x.toString()); + return out; +`; + +test("switch produces optimal output", () => { + optimalOutput(0).expectLuaToMatchSnapshot(); +}); + +test.each([0, 1, 2, 3, 4, 5])("switch produces valid optimal output (%p)", inp => { + optimalOutput(inp).expectToMatchJsResult(); +}); + +describe("switch hoisting", () => { + test("hoisting between cases", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + case 1: + result = hoisted(); + break; + case 2: + function hoisted() { + return "hoisted"; + } + break; + } + return result; + `.expectToMatchJsResult(); + }); + + test("indirect hoisting between cases", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + case 1: + function callHoisted() { + return hoisted(); + } + result = callHoisted(); + break; + case 2: + function hoisted() { + return "hoisted"; + } + break; + } + return result; + `.expectToMatchJsResult(); + }); + + test("hoisting in case expression", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + case hoisted(): + result = "hoisted"; + break; + case 2: + function hoisted() { + return 1; + } + break; + } + return result; + `.expectToMatchJsResult(); + }); + + test("hoisting from default clause", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + case 1: + result = hoisted(); + break; + default: + function hoisted() { + return "hoisted"; + } + break; + } + return result; + `.expectToMatchJsResult(); + }); + + test("hoisting from default clause is not duplicated when falling through", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + case 1: + result = hoisted(); + break; + case 2: + result = "2"; + default: + function hoisted() { + return "hoisted"; + } + result = "default"; + case 3: + result = "3"; + } + return result; + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + }); + + test("hoisting from fallthrough clause after default is not duplicated", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + case 1: + result = hoisted(); + break; + case 2: + result = "2"; + default: + result = "default"; + case 3: + function hoisted() { + return "hoisted"; + } + result = "3"; + } + return result; + ` + .expectToMatchJsResult() + .expectLuaToMatchSnapshot(); + }); + + test("hoisting in a solo default clause", () => { + util.testFunction` + let x = 1; + let result = ""; + switch (x) { + default: + result = hoisted(); + function hoisted() { + return "hoisted"; + } + } + return result; + `.expectToMatchJsResult(); + }); +}); diff --git a/test/unit/templateLiterals.spec.ts b/test/unit/templateLiterals.spec.ts new file mode 100644 index 000000000..318804ee9 --- /dev/null +++ b/test/unit/templateLiterals.spec.ts @@ -0,0 +1,108 @@ +import * as util from "../util"; + +test.each([ + { a: 12, b: 23, c: 43 }, + { a: "test", b: "hello", c: "bye" }, + { a: "test", b: 42, c: "bye" }, + { a: "test", b: 42, c: 12 }, + { a: "test", b: 42, c: true }, + { a: false, b: 42, c: 12 }, +])("template literal (%p)", ({ a, b, c }) => { + util.testExpressionTemplate`\`\${${a}} \${${b}} test \${${c}}\``.expectToMatchJsResult(); +}); + +test.each(["a++", "a--", "--a", "++a"])("template literal with expression (%p)", expression => { + util.testFunction` + let a = 3; + return \`value\${${expression}}\`; + `.expectToMatchJsResult(); +}); + +test.each(["`foo${'bar'}`.length", "`foo${'bar'}`.repeat(2)"])("template literal property access (%p)", expression => { + util.testExpression(expression).expectToMatchJsResult(); +}); + +test.each([ + "func``", + "func`hello`", + "func`hello ${1} ${2} ${3}`", + "func`hello ${(() => 'iife')()}`", + "func`hello ${1 + 2 + 3} arithmetic`", + "func`begin ${'middle'} end`", + "func`hello ${func`hello`}`", + "func`hello \\u00A9`", + "func`hello $ { }`", + "func`hello { ${'brackets'} }`", + "func`hello \\``", + "obj.func`hello ${'propertyAccessExpression'}`", + "obj['func']`hello ${'elementAccessExpression'}`", +])("tagged template literal (%p)", expression => { + util.testFunction` + function func(strings: TemplateStringsArray, ...expressions: any[]) { + return { strings: [...strings], raw: strings.raw, expressions }; + } + + const obj = { func }; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test.each(["func`noSelfParameter`", "obj.func`noSelfParameter`", "obj[`func`]`noSelfParameter`"])( + "tagged template literal function context (%p)", + expression => { + util.testFunction` + function func(this: void, strings: TemplateStringsArray) { + return [...strings]; + } + + const obj = { func }; + return ${expression}; + `.expectToMatchJsResult(); + } +); + +test.each(["number", "string | any"])("template span expect tostring for non string type (%p)", type => { + util.testFunction` + // @ts-ignore + const msg = "" as ${type}; + return \`\${msg}\`; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("tostring")) + .expectToMatchJsResult(); +}); + +test.each(["string", "'string literal type'", "string & unknown"])( + "template span expect no tostring for string-like type (%p)", + type => { + util.testFunction` + // @ts-ignore + const msg = "" as ${type}; + return \`\${msg}\`; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("tostring")) + .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/tshelper.spec.ts b/test/unit/tshelper.spec.ts deleted file mode 100644 index 408443d8e..000000000 --- a/test/unit/tshelper.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import { TSHelper as tsHelper } from "../../src/TSHelper"; - -import * as ts from "typescript"; -import * as util from "../src/util"; - -import { DecoratorKind } from "../../src/Decorator"; - -enum TestEnum { - testA = 1, - testB = 2, - testC = 4, -} - -export class TSHelperTests { - - @TestCase(TestEnum.testA, "testA") - @TestCase(-1, "unknown") - @TestCase(TestEnum.testA | TestEnum.testB, "unknown") - @Test("EnumName") - public testEnumName(inp, expected): void { - const result = tsHelper.enumName(inp, TestEnum); - - Expect(result).toEqual(expected); - } - - @TestCase(TestEnum.testA, ["testA"]) - @TestCase(-1, []) - @TestCase(TestEnum.testA | TestEnum.testC, ["testA", "testC"]) - @TestCase(TestEnum.testA | TestEnum.testB | TestEnum.testC, ["testA", "testB", "testC"]) - @Test("EnumNames") - public testEnumNames(inp, expected): void { - const result = tsHelper.enumNames(inp, TestEnum); - - Expect(result).toEqual(expected); - } - - @Test("IsFileModuleNull") - public isFileModuleNull(): void { - Expect(tsHelper.isFileModule(undefined)).toEqual(false); - } - - @Test("GetCustomDecorators single") - public GetCustomDecoratorsSingle(): void { - const source = `/** @compileMembersOnly */ - enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", - } - - const a = TestEnum.val1;`; - - const [sourceFile, typeChecker] = util.parseTypeScript(source); - const identifier = util.findFirstChild(sourceFile, ts.isEnumDeclaration); - const enumType = typeChecker.getTypeAtLocation(identifier); - - const decorators = tsHelper.getCustomDecorators(enumType, typeChecker); - - Expect(decorators.size).toBe(1); - Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy(); - } - - @Test("GetCustomDecorators multiple") - public GetCustomDecoratorsMultiple(): void { - const source = `/** @compileMembersOnly - * @Phantom */ - enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", - } - - const a = TestEnum.val1;`; - - const [sourceFile, typeChecker] = util.parseTypeScript(source); - const identifier = util.findFirstChild(sourceFile, ts.isEnumDeclaration); - const enumType = typeChecker.getTypeAtLocation(identifier); - - const decorators = tsHelper.getCustomDecorators(enumType, typeChecker); - - Expect(decorators.size).toBe(2); - Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy(); - Expect(decorators.has(DecoratorKind.Phantom)).toBeTruthy(); - } - - @Test("GetCustomDecorators single jsdoc") - public GetCustomDecoratorsSingleJSDoc(): void { - const source = `/** @compileMembersOnly */ - enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", - } - - const a = TestEnum.val1;`; - - const [sourceFile, typeChecker] = util.parseTypeScript(source); - const identifier = util.findFirstChild(sourceFile, ts.isEnumDeclaration); - const enumType = typeChecker.getTypeAtLocation(identifier); - - const decorators = tsHelper.getCustomDecorators(enumType, typeChecker); - - Expect(decorators.size).toBe(1); - Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy(); - } - - @Test("GetCustomDecorators multiple jsdoc") - public GetCustomDecoratorsMultipleJSDoc(): void { - const source = `/** @phantom - * @CompileMembersOnly */ - enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", - } - - const a = TestEnum.val1;`; - - const [sourceFile, typeChecker] = util.parseTypeScript(source); - const identifier = util.findFirstChild(sourceFile, ts.isEnumDeclaration); - const enumType = typeChecker.getTypeAtLocation(identifier); - - const decorators = tsHelper.getCustomDecorators(enumType, typeChecker); - - Expect(decorators.size).toBe(2); - Expect(decorators.has(DecoratorKind.Phantom)).toBeTruthy(); - Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy(); - } - - @Test("GetCustomDecorators multiple default jsdoc") - public GetCustomDecoratorsMultipleDefaultJSDoc(): void { - const source = `/** - * @description abc - * @phantom - * @compileMembersOnly */ - enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", - } - - const a = TestEnum.val1;`; - - const [sourceFile, typeChecker] = util.parseTypeScript(source); - const identifier = util.findFirstChild(sourceFile, ts.isEnumDeclaration); - const enumType = typeChecker.getTypeAtLocation(identifier); - - const decorators = tsHelper.getCustomDecorators(enumType, typeChecker); - - Expect(decorators.size).toBe(2); - Expect(decorators.has(DecoratorKind.Phantom)).toBeTruthy(); - Expect(decorators.has(DecoratorKind.CompileMembersOnly)).toBeTruthy(); - } -} diff --git a/test/unit/tuples.spec.ts b/test/unit/tuples.spec.ts deleted file mode 100644 index 16f1bf935..000000000 --- a/test/unit/tuples.spec.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; -import * as util from "../src/util"; - -export class TupleTests { - @Test("Tuple loop") - public tupleLoop(): void - { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - let count = 0; - for (const value of tuple) { count += value; } - return count;` - ); - - // Assert - Expect(result).toBe(9); - } - - @Test("Tuple foreach") - public tupleForEach(): void - { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - let count = 0; - tuple.forEach(v => count += v); - return count;` - ); - - // Assert - Expect(result).toBe(9); - } - - @Test("Tuple access") - public tupleAccess(): void - { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - return tuple[1];` - ); - - // Assert - Expect(result).toBe(5); - } - - @Test("Tuple union access") - public tupleUnionAccess(): void - { - const result = util.transpileAndExecute( - `function makeTuple(): [number, number, number] | [string, string, string] { return [3,5,1]; } - const tuple = makeTuple(); - return tuple[1];` - ); - Expect(result).toBe(5); - } - - @Test("Tuple intersection access") - public tupleIntersectionAccess(): void - { - const result = util.transpileAndExecute( - `type I = [number, number, number] & {foo: string}; - function makeTuple(): I { - let t = [3,5,1]; - (t as I).foo = "bar"; - return (t as I); - } - const tuple = makeTuple(); - return tuple[1];` - ); - Expect(result).toBe(5); - } - - @Test("Tuple Destruct") - public tupleDestruct(): void - { - const result = util.transpileAndExecute( - `function tuple(): [number, number, number] { return [3,5,1]; } - const [a,b,c] = tuple(); - return b;` - ); - - // Assert - Expect(result).toBe(5); - } - - @Test("Tuple length") - public tupleLength(): void - { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - return tuple.length;` - ); - - // Assert - Expect(result).toBe(3); - } - - @Test("Tuple Return Access") - public tupleReturnAccess(): void - { - const result = util.transpileAndExecute( - `/** @tupleReturn */ - function tuple(): [number, number, number] { return [3,5,1]; } - return tuple()[2];` - ); - - // Assert - Expect(result).toBe(1); - } - - @Test("Tuple Return Destruct Declaration") - public tupleReturnDestructDeclaration(): void - { - const result = util.transpileAndExecute( - `/** @tupleReturn */ - function tuple(): [number, number, number] { return [3,5,1]; } - const [a,b,c] = tuple(); - return b;` - ); - - // Assert - Expect(result).toBe(5); - } - - @Test("Tuple Return Destruct Assignment") - public tupleReturnDestructAssignment(): void - { - const result = util.transpileAndExecute( - `/** @tupleReturn */ - function tuple(): [number, number] { return [3,6]; } - let [a,b] = [1,2]; - [b,a] = tuple(); - return a - b;` - ); - - // Assert - Expect(result).toBe(3); - } - - @Test("Tuple Static Method Return Destruct") - public tupleStaticMethodReturnDestruct(): void - { - const result = util.transpileAndExecute( - `class Test { - /** @tupleReturn */ - static tuple(): [number, number, number] { return [3,5,1]; } - } - const [a,b,c] = Test.tuple(); - return b;` - ); - - // Assert - Expect(result).toBe(5); - } - - @Test("Tuple Non-Static Method Return Destruct") - public tupleMethodNonStaticReturnDestruct(): void - { - const result = util.transpileAndExecute( - `class Test { - /** @tupleReturn */ - tuple(): [number, number, number] { return [3,5,1]; } - } - const t = new Test(); - const [a,b,c] = t.tuple(); - return b;` - ); - - // Assert - Expect(result).toBe(5); - } -} diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts deleted file mode 100644 index 533c0d3d0..000000000 --- a/test/unit/typechecking.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Expect, Test, TestCase } from "alsatian"; - -import * as util from "../src/util"; -import { TranspileError } from "../../src/TranspileError"; - -export class TypeCheckingTests { - @TestCase("0") - @TestCase("30") - @TestCase("30_000") - @TestCase("30.00") - @Test("typeof number") - public typeofNumberTest(inp: string): void - { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - Expect(result).toBe("number"); - } - - @TestCase("\"abc\"") - @TestCase("`abc`") - @Test("typeof string") - public typeofStringTest(inp: string): void - { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - Expect(result).toBe("string"); - } - - @TestCase("false") - @TestCase("true") - @Test("typeof boolean") - public typeofBooleanTest(inp: string): void - { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - Expect(result).toBe("boolean"); - } - - @TestCase("{}") - @TestCase("[]") - @Test("typeof object literal") - public typeofObjectLiteral(inp: string): void - { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - Expect(result).toBe("object"); - } - - @Test("typeof class instance") - public typeofClassInstance(): void - { - const result = util.transpileAndExecute(`class myClass {} let inst = new myClass(); return typeof inst;`); - - Expect(result).toBe("object"); - } - - @Test("typeof function") - public typeofFunction(): void - { - const result = util.transpileAndExecute(`return typeof (() => 3);`); - - Expect(result).toBe("function"); - } - - @TestCase("null") - @TestCase("undefined") - @Test("typeof undefined") - public typeofUndefinedTest(inp: string): void - { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - Expect(result).toBe("nil"); - } - - @Test("instanceof") - public instanceOf(): void - { - const result = util.transpileAndExecute( - "class myClass {} let inst = new myClass(); return inst instanceof myClass;" - ); - - Expect(result).toBeTruthy(); - } - - @Test("instanceof inheritance") - public instanceOfInheritance(): void - { - const result = util.transpileAndExecute("class myClass {}\n" - + "class childClass extends myClass{}\n" - + "let inst = new childClass(); return inst instanceof myClass;"); - - Expect(result).toBeTruthy(); - } - - @Test("instanceof inheritance false") - public instanceOfInheritanceFalse(): void - { - const result = util.transpileAndExecute("class myClass {}\n" - + "class childClass extends myClass{}\n" - + "let inst = new myClass(); return inst instanceof childClass;"); - - Expect(result).toBe(false); - } - - @Test("null instanceof Object") - public nullInstanceOf(): void - { - const result = util.transpileAndExecute("return (null) instanceof Object;"); - - Expect(result).toBe(false); - } - - @Test("null instanceof Class") - public nullInstanceOfClass(): void - { - const result = util.transpileAndExecute("class myClass {} return (null) instanceof myClass;"); - - Expect(result).toBe(false); - } - - @TestCase("extension") - @TestCase("metaExtension") - @Test("instanceof extension") - public instanceOfExtension(extensionType: string): void { - const code = - `declare class A {} - /** @${extensionType} **/ - class B extends A {} - declare const foo: any; - const result = foo instanceof B;`; - Expect(() => util.transpileString(code)).toThrowError( - TranspileError, - "Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'." - ); - } - - @Test("instanceof export") - public instanceOfExport(): void - { - const result = util.transpileExecuteAndReturnExport( - `export class myClass {} - let inst = new myClass(); - export const result = inst instanceof myClass;`, - "result" - ); - - Expect(result).toBeTruthy(); - } -} diff --git a/test/unit/typeof.spec.ts b/test/unit/typeof.spec.ts new file mode 100644 index 000000000..f8a063c1c --- /dev/null +++ b/test/unit/typeof.spec.ts @@ -0,0 +1,99 @@ +import * as util from "../util"; + +test.each(["0", "30", "30_000", "30.00"])("typeof number (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test.each(['"abc"', "`abc`"])("typeof string (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test.each(["false", "true"])("typeof boolean (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test.each(["{}", "[]"])("typeof object literal (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test("typeof class", () => { + util.testFunction` + class MyClass {} + return typeof MyClass; + `.expectToEqual("object"); +}); + +test("typeof class instance", () => { + util.testFunction` + class MyClass {} + return typeof new MyClass(); + `.expectToMatchJsResult(); +}); + +test("typeof function", () => { + util.testExpression`typeof (() => 3)`.expectToMatchJsResult(); +}); + +test.each(["null", "undefined"])("typeof %s", inp => { + util.testExpression`typeof ${inp}`.expectToEqual("undefined"); +}); + +interface ComparisonCase { + expression: string; + operator: string; + compareTo: string; +} + +const equalityComparisonCases: ComparisonCase[] = [ + { expression: "{}", operator: "===", compareTo: "object" }, + { expression: "{}", operator: "!==", compareTo: "object" }, + { expression: "{}", operator: "==", compareTo: "object" }, + { expression: "{}", operator: "!=", compareTo: "object" }, + { expression: "undefined", operator: "===", compareTo: "undefined" }, + { expression: "() => {}", operator: "===", compareTo: "function" }, + { expression: "1", operator: "===", compareTo: "number" }, + { expression: "true", operator: "===", compareTo: "boolean" }, + { expression: '"foo"', operator: "===", compareTo: "string" }, +]; + +const relationalComparisonCases: ComparisonCase[] = [ + { expression: "undefined", operator: "<=", compareTo: "object" }, + { expression: "undefined", operator: "<", compareTo: "object" }, + { expression: "undefined", operator: ">=", compareTo: "object" }, + { expression: "undefined", operator: ">", compareTo: "object" }, +]; + +const expectTypeOfHelper: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toMatch("__TS__TypeOf"); +const expectNoTypeOfHelper: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).not.toMatch("__TS__TypeOf"); + +test.each(equalityComparisonCases)("typeof literal equality comparison (%p)", ({ expression, operator, compareTo }) => { + util.testFunction` + const value = ${expression}; + return typeof value ${operator} "${compareTo}"; + ` + .tap(expectNoTypeOfHelper) + .expectToMatchJsResult(); +}); + +test.each(relationalComparisonCases)("typeof literal comparison (%p)", ({ expression, operator, compareTo }) => { + util.testFunction` + const value = ${expression}; + return typeof value ${operator} "${compareTo}"; + ` + .tap(expectTypeOfHelper) + .expectToMatchJsResult(); +}); + +test.each([...equalityComparisonCases, ...relationalComparisonCases])( + "typeof non-literal comparison (%p)", + ({ expression, operator, compareTo }) => { + util.testFunction` + const value = ${expression}; + const compareTo = "${compareTo}"; + return typeof value ${operator} compareTo; + ` + .tap(expectTypeOfHelper) + .expectToMatchJsResult(); + } +); diff --git a/test/unit/using.spec.ts b/test/unit/using.spec.ts new file mode 100644 index 000000000..bfe5312b0 --- /dev/null +++ b/test/unit/using.spec.ts @@ -0,0 +1,235 @@ +import { LuaLibImportKind } from "../../src"; +import * as util from "../util"; + +const usingTestLib = ` + export const logs: string[] = []; + + function loggedDisposable(id: string): Disposable { + logs.push(\`Creating \${id}\`); + + return { + [Symbol.dispose]() { + logs.push(\`Disposing \${id}\`); + } + } + }`; + +test("using disposes object at end of function", () => { + util.testModule` + function func() { + using a = loggedDisposable("a"); + using b = loggedDisposable("b"); + + logs.push("function content"); + } + + func(); + ` + .setTsHeader(usingTestLib) + .expectToEqual({ logs: ["Creating a", "Creating b", "function content", "Disposing b", "Disposing a"] }); +}); + +test("handles multi-variable declarations", () => { + util.testModule` + function func() { + using a = loggedDisposable("a"), b = loggedDisposable("b"); + + logs.push("function content"); + } + + func(); + ` + .setTsHeader(usingTestLib) + .expectToEqual({ logs: ["Creating a", "Creating b", "function content", "Disposing b", "Disposing a"] }); +}); + +test("using disposes object at end of nested block", () => { + util.testModule` + function func() { + using a = loggedDisposable("a"); + + { + using b = loggedDisposable("b"); + logs.push("nested block"); + } + + logs.push("function content"); + } + + func(); + ` + .setTsHeader(usingTestLib) + .expectToEqual({ + logs: ["Creating a", "Creating b", "nested block", "Disposing b", "function content", "Disposing a"], + }); +}); + +test("using does not affect function return value", () => { + util.testModule` + function func() { + using a = loggedDisposable("a"); + using b = loggedDisposable("b"); + + logs.push("function content"); + + return "success"; + } + + export const result = func(); + ` + .setTsHeader(usingTestLib) + .expectToEqual({ + result: "success", + logs: ["Creating a", "Creating b", "function content", "Disposing b", "Disposing a"], + }); +}); + +test("using disposes even when error happens", () => { + util.testModule` + function func() { + using a = loggedDisposable("a"); + using b = loggedDisposable("b"); + + throw "test-induced exception"; + } + + try + { + func(); + } + catch (e) + { + logs.push(\`caught exception: \${e}\`); + } + ` + .setTsHeader(usingTestLib) + .expectToEqual({ + logs: [ + "Creating a", + "Creating b", + "Disposing b", + "Disposing a", + "caught exception: test-induced exception", + ], + }); +}); + +test("await using disposes object with await at end of function", () => { + util.testModule` + let disposeAsync; + + function loggedAsyncDisposable(id: string): AsyncDisposable { + logs.push(\`Creating \${id}\`); + + return { + [Symbol.asyncDispose]() { + logs.push(\`Disposing async \${id}\`); + return new Promise(resolve => { + disposeAsync = () => { + logs.push(\`Disposed \${id}\`); + resolve(); + }; + }); + } + } + } + + async function func() { + await using a = loggedAsyncDisposable("a"); + + logs.push("function content"); + return "function result"; + } + + const p = func().then(r => logs.push("promise resolved", r)); + + logs.push("function returned"); + + disposeAsync(); + ` + .setTsHeader(usingTestLib) + .setOptions({ luaLibImport: LuaLibImportKind.Inline }) + .expectToEqual({ + logs: [ + "Creating a", + "function content", + "Disposing async a", + "function returned", + "Disposed a", + "promise resolved", + "function result", + ], + }); +}); + +test("await using can handle non-async disposables", () => { + util.testModule` + async function func() { + await using a = loggedDisposable("a"); + + logs.push("function content"); + } + + func(); + ` + .setTsHeader(usingTestLib) + .expectToEqual({ logs: ["Creating a", "function content", "Disposing a"] }); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1571 +test("await using no extra diagnostics (#1571)", () => { + util.testModule` + async function getResource(): Promise { + return { + [Symbol.asyncDispose]: async () => {} + }; + } + + async function someOtherAsync() {} + + async function main() { + await using resource = await getResource(); + await someOtherAsync(); + } + `.expectToHaveNoDiagnostics(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1584 +test("works with disposable classes (#1584)", () => { + util.testFunction` + const log = []; + + class Scoped { + action(): void { + log.push("action") + } + [Symbol.dispose]() { + log.push("cleanup") + } + } + + function TestScoped(): void { + using s = new Scoped(); + s.action(); + } + + TestScoped(); + 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/unit/void.spec.ts b/test/unit/void.spec.ts new file mode 100644 index 000000000..024f0cf7e --- /dev/null +++ b/test/unit/void.spec.ts @@ -0,0 +1,34 @@ +import * as util from "../util"; + +test.each(["0", "1", '"a"'])("void evaluates to undefined (%p)", value => { + util.testExpression`void (${value})`.expectToMatchJsResult(); +}); + +test("void applies to function declarations", () => { + util.testFunction` + let result = 0; + void function setResult() { + result = 1; + }(); + return result; + `.expectToMatchJsResult(); +}); + +test("void used to ignore function return values", () => { + util.testFunction` + let result = 0; + function setResult() { + result = 1; + return 3 + }; + + void(setResult()); + + return result; + `.expectToMatchJsResult(); +}); + +// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1102 +test("void works with lambdas", () => { + util.testExpression`void (() => {})()`.expectToMatchJsResult(); +}); diff --git a/test/util.ts b/test/util.ts new file mode 100644 index 000000000..2871f6e5a --- /dev/null +++ b/test/util.ts @@ -0,0 +1,683 @@ +/* eslint-disable jest/no-standalone-expect */ +import * as nativeAssert from "assert"; +import { LauxLib, Lua, LuaLib, LuaState, LUA_OK } from "lua-wasm-bindings/dist/lua"; +import * as fs from "fs"; +import { stringify } from "javascript-stringify"; +import * as path from "path"; +import * as prettyFormat from "pretty-format"; +import * as ts from "typescript"; +import * as vm from "vm"; +import * as tstl from "../src"; +import { createEmitOutputCollector } from "../src/transpilation/output-collector"; +import { EmitHost, getEmitOutDir, transpileProject } from "../src"; +import { formatPathToLuaPath, normalizeSlashes } from "../src/utils"; +import { resolveLuaLibDir } from "../src/LuaLib"; + +function readLuaLib(target: tstl.LuaTarget) { + return fs.readFileSync(path.join(resolveLuaLibDir(target), "lualib_bundle.lua"), "utf8"); +} + +function jsonLib(target: tstl.LuaTarget): string { + const fileName = target === tstl.LuaTarget.Lua50 ? "json.50.lua" : "json.lua"; + return fs.readFileSync(path.join(__dirname, fileName), "utf8"); +} + +// Using `test` directly makes eslint-plugin-jest consider this file as a test +const defineTest = test; + +function getLuaBindingsForVersion(target: tstl.LuaTarget): { lauxlib: LauxLib; lua: Lua; lualib: LuaLib } { + if (target === tstl.LuaTarget.Lua50) { + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.50"); + return { lauxlib, lua, lualib }; + } + if (target === tstl.LuaTarget.Lua51) { + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.51"); + return { lauxlib, lua, lualib }; + } + if (target === tstl.LuaTarget.Lua52) { + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.52"); + return { lauxlib, lua, lualib }; + } + if (target === tstl.LuaTarget.Lua53) { + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.53"); + return { lauxlib, lua, lualib }; + } + if (target === tstl.LuaTarget.LuaJIT) { + throw Error("Can't use executeLua() or expectToMatchJsResult() with LuaJIT as target!"); + } + + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.54"); + return { lauxlib, lua, lualib }; +} + +export function assert(value: any, message?: string | Error): asserts value { + nativeAssert(value, message); +} + +export const formatCode = (...values: unknown[]) => values.map(e => stringify(e)).join(", "); + +export function testEachVersion( + name: string | undefined, + common: () => T, + special?: Record void) | boolean> +): void { + for (const version of Object.values(tstl.LuaTarget) as tstl.LuaTarget[]) { + const specialBuilder = special?.[version]; + if (specialBuilder === false) continue; + + const testName = name === undefined ? version : `${name} [${version}]`; + defineTest(testName, () => { + const builder = common(); + builder.setOptions({ luaTarget: version }); + if (typeof specialBuilder === "function") { + specialBuilder(builder); + } + }); + } +} + +export function expectEachVersionExceptJit( + expectation: (builder: T) => void +): Record void) | boolean> { + return { + [tstl.LuaTarget.Universal]: expectation, + [tstl.LuaTarget.Lua50]: expectation, + [tstl.LuaTarget.Lua51]: expectation, + [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 memoized = new WeakMap(); + descriptor.value = 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 { + public name = "ExecutionError"; + // https://github.com/typescript-eslint/typescript-eslint/issues/1131 + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(message: string) { + super(message); + } +} + +export type ExecutableTranspiledFile = tstl.TranspiledFile & { lua: string; luaSourceMap: string }; +export type TapCallback = (builder: TestBuilder) => void; +export abstract class TestBuilder { + constructor(protected _tsCode: string) {} + + // Options + + // TODO: Use testModule in these cases? + protected tsHeader = ""; + public setTsHeader(tsHeader: string): this { + this.throwIfProgramExists("setTsHeader"); + this.tsHeader = tsHeader; + return this; + } + + private luaHeader = ""; + public setLuaHeader(luaHeader: string): this { + this.throwIfProgramExists("setLuaHeader"); + this.luaHeader += luaHeader; + return this; + } + + protected jsHeader = ""; + public setJsHeader(jsHeader: string): this { + this.throwIfProgramExists("setJsHeader"); + this.jsHeader += jsHeader; + return this; + } + + protected abstract getLuaCodeWithWrapper(code: string): string; + public setLuaFactory(luaFactory: (code: string) => string): this { + this.throwIfProgramExists("setLuaFactory"); + this.getLuaCodeWithWrapper = luaFactory; + return this; + } + + private semanticCheck = true; + public disableSemanticCheck(): this { + this.throwIfProgramExists("disableSemanticCheck"); + this.semanticCheck = false; + return this; + } + + protected options: tstl.CompilerOptions = { + luaTarget: tstl.LuaTarget.Lua55, + noHeader: true, + skipLibCheck: true, + target: ts.ScriptTarget.ES2017, + lib: ["lib.esnext.d.ts"], + moduleResolution: ts.ModuleResolutionKind.Node10, + resolveJsonModule: true, + sourceMap: true, + }; + public setOptions(options: tstl.CompilerOptions = {}): this { + this.throwIfProgramExists("setOptions"); + Object.assign(this.options, options); + return this; + } + + public withLanguageExtensions(): this { + const langExtTypes = path.resolve(__dirname, "..", "language-extensions"); + this.options.types = this.options.types ? [...this.options.types, langExtTypes] : [langExtTypes]; + // Polyfill lualib for JS + this.setJsHeader(` + function $multi(...args) { return args; } + `); + return this; + } + + protected mainFileName = "main.ts"; + public setMainFileName(mainFileName: string): this { + this.throwIfProgramExists("setMainFileName"); + this.mainFileName = mainFileName; + return this; + } + + protected extraFiles: Record = {}; + public addExtraFile(fileName: string, code: string): this { + this.throwIfProgramExists("addExtraFile"); + this.extraFiles[fileName] = normalizeSlashes(code); + return this; + } + + private customTransformers?: ts.CustomTransformers; + public setCustomTransformers(customTransformers?: ts.CustomTransformers): this { + this.throwIfProgramExists("setCustomTransformers"); + this.customTransformers = customTransformers; + return this; + } + + private throwIfProgramExists(name: string) { + if (this.hasProgram) { + throw new Error(`${name}() should not be called after an .expect() or .debug()`); + } + } + + // Transpilation and execution + + public getTsCode(): string { + return `${this.tsHeader}${this._tsCode}`; + } + + protected hasProgram = false; + @memoize + public getProgram(): ts.Program { + this.hasProgram = true; + + // Exclude lua files from TS program, but keep them in extraFiles so module resolution can find them + const nonLuaExtraFiles = Object.fromEntries( + Object.entries(this.extraFiles).filter(([fileName]) => !fileName.endsWith(".lua")) + ); + + return tstl.createVirtualProgram({ ...nonLuaExtraFiles, [this.mainFileName]: this.getTsCode() }, this.options); + } + + private getEmitHost(): EmitHost { + return { + fileExists: (path: string) => normalizeSlashes(path) in this.extraFiles, + directoryExists: (path: string) => + Object.keys(this.extraFiles).some(f => f.startsWith(normalizeSlashes(path))), + getCurrentDirectory: () => ".", + readFile: (path: string) => this.extraFiles[normalizeSlashes(path)] ?? ts.sys.readFile(path), + writeFile() {}, + }; + } + + @memoize + public getLuaResult(): tstl.TranspileVirtualProjectResult { + const program = this.getProgram(); + const preEmitDiagnostics = ts.getPreEmitDiagnostics(program); + const collector = createEmitOutputCollector(this.options.extension); + const { diagnostics: transpileDiagnostics } = new tstl.Transpiler({ emitHost: this.getEmitHost() }).emit({ + program, + customTransformers: this.customTransformers, + writeFile: collector.writeFile, + }); + + const diagnostics = ts.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics]); + + return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; + } + + @memoize + public getMainLuaFileResult(): ExecutableTranspiledFile { + const { transpiledFiles } = this.getLuaResult(); + const mainFileName = normalizeSlashes(this.mainFileName); + const mainFile = this.options.luaBundle + ? transpiledFiles[0] + : transpiledFiles.find(({ sourceFiles }) => sourceFiles.some(f => f.fileName === mainFileName)); + + if (mainFile === undefined) { + throw new Error( + `No source file could be found matching main file: ${mainFileName}.\nSource files in test:\n${transpiledFiles + .flatMap(f => f.sourceFiles.map(sf => sf.fileName)) + .join("\n")}` + ); + } + + expect(mainFile).toMatchObject({ lua: expect.any(String), luaSourceMap: expect.any(String) }); + return mainFile as ExecutableTranspiledFile; + } + + @memoize + public getMainLuaCodeChunk(): string { + const header = this.luaHeader ? `${this.luaHeader.trimRight()}\n` : ""; + return header + this.getMainLuaFileResult().lua.trimRight(); + } + + @memoize + public getLuaExecutionResult(): any { + return this.executeLua(); + } + + @memoize + public getJsResult(): tstl.TranspileVirtualProjectResult { + const program = this.getProgram(); + program.getCompilerOptions().module = ts.ModuleKind.CommonJS; + + const collector = createEmitOutputCollector(this.options.extension); + const { diagnostics } = program.emit(undefined, collector.writeFile); + return { transpiledFiles: collector.files, diagnostics: [...diagnostics] }; + } + + @memoize + public getMainJsCodeChunk(): string { + const { transpiledFiles } = this.getJsResult(); + const code = transpiledFiles.find(({ sourceFiles }) => + sourceFiles.some(f => f.fileName === this.mainFileName) + )?.js; + assert(code !== undefined); + + const header = this.jsHeader ? `${this.jsHeader.trimRight()}\n` : ""; + return header + code; + } + + protected abstract getJsCodeWithWrapper(): string; + + @memoize + public getJsExecutionResult(): any { + return this.executeJs(); + } + + // Utilities + + private getLuaDiagnostics(): ts.Diagnostic[] { + const { diagnostics } = this.getLuaResult(); + return diagnostics.filter( + d => (this.semanticCheck || d.source === "typescript-to-lua") && !this.ignoredDiagnostics.includes(d.code) + ); + } + + // Actions + + public debug(includeLualib = false): this { + const { transpiledFiles, diagnostics } = this.getLuaResult(); + const luaCode = transpiledFiles + .filter(f => includeLualib || f.outPath !== "lualib_bundle.lua") + .map(f => `[${f.outPath}]:\n${f.lua?.replace(/^/gm, " ")}`); + const value = prettyFormat.format(this.getLuaExecutionResult()).replace(/^/gm, " "); + console.log(`Lua Code:\n${luaCode.join("\n")}\n\nValue:\n${value}`); + + if (diagnostics.length > 0) { + console.log( + ts.formatDiagnostics(diagnostics.map(tstl.prepareDiagnosticForFormatting), { + getCurrentDirectory: () => "", + getCanonicalFileName: fileName => fileName, + getNewLine: () => "\n", + }) + ); + } + + return this; + } + + private diagnosticsChecked = false; + private ignoredDiagnostics: number[] = []; + + public ignoreDiagnostics(ignored: number[]): this { + this.ignoredDiagnostics.push(...ignored); + return this; + } + + public expectToHaveDiagnostics(expected?: number[]): this { + if (this.diagnosticsChecked) return this; + this.diagnosticsChecked = true; + + expect(this.getLuaDiagnostics()).toHaveDiagnostics(expected); + return this; + } + + public expectToHaveNoDiagnostics(): this { + if (this.diagnosticsChecked) return this; + this.diagnosticsChecked = true; + + expect(this.getLuaDiagnostics()).not.toHaveDiagnostics(); + return this; + } + + public expectNoTranspileException(): this { + expect(() => this.getLuaResult()).not.toThrow(); + return this; + } + + public expectNoExecutionError(): this { + const luaResult = this.getLuaExecutionResult(); + if (luaResult instanceof ExecutionError) { + throw luaResult; + } + + return this; + } + + private expectNoJsExecutionError(): this { + const jsResult = this.getJsExecutionResult(); + if (jsResult instanceof ExecutionError) { + throw jsResult; + } + + return this; + } + + public expectToMatchJsResult(allowErrors = false): this { + this.expectToHaveNoDiagnostics(); + if (!allowErrors) this.expectNoExecutionError(); + if (!allowErrors) this.expectNoJsExecutionError(); + + const luaResult = this.getLuaExecutionResult(); + const jsResult = this.getJsExecutionResult(); + expect(luaResult).toEqual(jsResult); + + return this; + } + + public expectToEqual(expected: any): this { + this.expectToHaveNoDiagnostics(); + const luaResult = this.getLuaExecutionResult(); + expect(luaResult).toEqual(expected); + return this; + } + + public expectLuaToMatchSnapshot(): this { + this.expectToHaveNoDiagnostics(); + expect(this.getMainLuaCodeChunk()).toMatchSnapshot(); + return this; + } + + public expectDiagnosticsToMatchSnapshot(expected?: number[], diagnosticsOnly = false): this { + this.expectToHaveDiagnostics(expected); + + const diagnosticMessages = ts.formatDiagnostics( + this.getLuaDiagnostics().map(tstl.prepareDiagnosticForFormatting), + { getCurrentDirectory: () => "", getCanonicalFileName: fileName => fileName, getNewLine: () => "\n" } + ); + + expect(diagnosticMessages.trim()).toMatchSnapshot("diagnostics"); + if (!diagnosticsOnly) { + expect(this.getMainLuaCodeChunk()).toMatchSnapshot("code"); + } + + return this; + } + + public tap(callback: TapCallback): this { + callback(this); + return this; + } + + private executeLua(): any { + // Main file + const mainFile = this.getMainLuaCodeChunk(); + + const luaTarget = this.options.luaTarget ?? tstl.LuaTarget.Lua55; + const { lauxlib, lua, lualib } = getLuaBindingsForVersion(luaTarget); + + const L = lauxlib.luaL_newstate(); + lualib.luaL_openlibs(L); + + // Load modules + // Json + this.injectLuaFile(L, lua, lauxlib, "json", jsonLib(luaTarget)); + // Lua lib + if ( + this.options.luaLibImport === tstl.LuaLibImportKind.Require || + mainFile.includes('require("lualib_bundle")') + ) { + this.injectLuaFile(L, lua, lauxlib, "lualib_bundle", readLuaLib(luaTarget)); + } + + // Load all transpiled files into Lua's package cache + const { transpiledFiles } = this.getLuaResult(); + for (const transpiledFile of transpiledFiles) { + if (transpiledFile.lua) { + const filePath = path.relative(getEmitOutDir(this.getProgram()), transpiledFile.outPath); + this.injectLuaFile(L, lua, lauxlib, filePath, transpiledFile.lua); + } + } + + // Execute Main + const wrappedMainCode = ` +local JSON = require("json"); +return JSON.stringify((function() + ${this.getLuaCodeWithWrapper(mainFile)} +end)());`; + + const status = lauxlib.luaL_dostring(L, wrappedMainCode); + + if (status === LUA_OK) { + if (lua.lua_isstring(L, -1)) { + const result = eval(`(${lua.lua_tostring(L, -1)})`); + lua.lua_close(L); + return result === null ? undefined : result; + } else { + const returnType = lua.lua_typename(L, lua.lua_type(L, -1)); + lua.lua_close(L); + throw new Error(`Unsupported Lua return type: ${returnType}`); + } + } else { + const luaStackString = lua.lua_tostring(L, -1); + const message = luaStackString.replace(/^\[string "(--)?\.\.\."\]:\d+: /, ""); + lua.lua_close(L); + return new ExecutionError(message); + } + } + + private injectLuaFile(state: LuaState, lua: Lua, lauxlib: LauxLib, fileName: string, fileContent: string) { + let extension = this.options.extension ?? ".lua"; + if (!extension.startsWith(".")) { + extension = `.${extension}`; + } + const modName = fileName.endsWith(extension) + ? formatPathToLuaPath(fileName.substring(0, fileName.length - extension.length)) + : fileName; + if (this.options.luaTarget === tstl.LuaTarget.Lua50) { + // Adding source Lua to the _LOADED cache will allow require to find it + lua.lua_getglobal(state, "_LOADED"); + lauxlib.luaL_dostring(state, fileContent); + lua.lua_setfield(state, -2, modName); + } else { + // Adding source Lua to the package.preload cache will allow require to find it + lua.lua_getglobal(state, "package"); + lua.lua_getfield(state, -1, "preload"); + lauxlib.luaL_loadstring(state, fileContent); + lua.lua_setfield(state, -2, modName); + } + } + + private executeJs(): any { + const { transpiledFiles } = this.getJsResult(); + // Custom require for extra files. Really basic. Global support is hacky + // TODO Should be replaced with vm.Module https://nodejs.org/api/vm.html#vm_class_vm_module + // once stable + const globalContext: any = {}; + const mainExports = {}; + globalContext.exports = mainExports; + globalContext.module = { exports: mainExports }; + globalContext.require = (fileName: string) => { + // create clean export object for "module" + const moduleExports = {}; + globalContext.exports = moduleExports; + globalContext.module = { exports: moduleExports }; + const transpiledExtraFile = transpiledFiles.find(({ sourceFiles }) => + sourceFiles.some(f => f.fileName === fileName.replace("./", "") + ".ts") + ); + + if (transpiledExtraFile?.js) { + vm.runInContext(transpiledExtraFile.js, globalContext); + } + + // Have to return globalContext.module.exports + // becuase module.exports might no longer be equal to moduleExports (export assignment) + const result = globalContext.module.exports; + // Reset module/export + globalContext.exports = mainExports; + globalContext.module = { exports: mainExports }; + return result; + }; + + vm.createContext(globalContext); + + let result: unknown; + try { + result = vm.runInContext(this.getJsCodeWithWrapper(), globalContext); + } catch (error) { + const hasMessage = (error: any): error is { message: string } => error.message !== undefined; + assert(hasMessage(error)); + return new ExecutionError(error.message); + } + + function removeUndefinedFields(obj: any): any { + if (obj === null) { + return undefined; + } + + if (Array.isArray(obj)) { + return obj.map(removeUndefinedFields); + } + + if (typeof obj === "object") { + const copy: any = {}; + for (const [key, value] of Object.entries(obj)) { + if (obj[key] !== undefined) { + copy[key] = removeUndefinedFields(value); + } + } + + if (Object.keys(copy).length === 0) { + return []; + } + + return copy; + } + + return obj; + } + + return removeUndefinedFields(result); + } +} + +class AccessorTestBuilder extends TestBuilder { + protected accessor = ""; + + protected getLuaCodeWithWrapper(code: string) { + return `return (function(...)\n${code}\nend)()${this.accessor}`; + } + + @memoize + protected getJsCodeWithWrapper(): string { + return this.getMainJsCodeChunk() + `\n;module.exports = module.exports${this.accessor}`; + } +} + +class BundleTestBuilder extends AccessorTestBuilder { + constructor(_tsCode: string) { + super(_tsCode); + this.setOptions({ luaBundle: "main.lua", luaBundleEntry: this.mainFileName }); + } + + public setEntryPoint(fileName: string): this { + return this.setOptions({ luaBundleEntry: fileName }); + } +} + +class ModuleTestBuilder extends AccessorTestBuilder { + public setReturnExport(...names: string[]): this { + expect(this.hasProgram).toBe(false); + this.accessor = names.map(n => `[${tstl.escapeString(n)}]`).join(""); + return this; + } +} +class FunctionTestBuilder extends AccessorTestBuilder { + protected accessor = ".__main()"; + public getTsCode(): string { + return `${this.tsHeader}export function __main() {${this._tsCode}}`; + } +} + +class ExpressionTestBuilder extends AccessorTestBuilder { + protected accessor = ".__result"; + public getTsCode(): string { + return `${this.tsHeader}export const __result = ${this._tsCode};`; + } +} + +class ProjectTestBuilder extends ModuleTestBuilder { + constructor(private tsConfig: string) { + super(""); + this.setOptions({ configFilePath: this.tsConfig, ...tstl.parseConfigFileWithSystem(this.tsConfig) }); + } + + @memoize + public getLuaResult(): tstl.TranspileVirtualProjectResult { + // Override getLuaResult to use transpileProject with tsconfig.json instead + const collector = createEmitOutputCollector(this.options.extension); + const { diagnostics } = transpileProject(this.tsConfig, this.options, collector.writeFile); + + return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; + } +} + +const createTestBuilderFactory = + (builder: new (_tsCode: string) => T, serializeSubstitutions: boolean) => + (...args: [string] | [TemplateStringsArray, ...any[]]): T => { + let tsCode: string; + if (typeof args[0] === "string") { + expect(serializeSubstitutions).toBe(false); + tsCode = args[0]; + } else { + let [raw, ...substitutions] = args; + if (serializeSubstitutions) { + substitutions = substitutions.map(s => formatCode(s)); + } + + tsCode = String.raw(Object.assign([], { raw }), ...substitutions); + } + + return new builder(tsCode); + }; + +export const testBundle = createTestBuilderFactory(BundleTestBuilder, false); +export const testModule = createTestBuilderFactory(ModuleTestBuilder, false); +export const testModuleTemplate = createTestBuilderFactory(ModuleTestBuilder, true); +export const testFunction = createTestBuilderFactory(FunctionTestBuilder, false); +export const testFunctionTemplate = createTestBuilderFactory(FunctionTestBuilder, true); +export const testExpression = createTestBuilderFactory(ExpressionTestBuilder, false); +export const testExpressionTemplate = createTestBuilderFactory(ExpressionTestBuilder, true); +export const testProject = createTestBuilderFactory(ProjectTestBuilder, false); diff --git a/tsconfig-schema.json b/tsconfig-schema.json new file mode 100644 index 000000000..6bffd5e46 --- /dev/null +++ b/tsconfig-schema.json @@ -0,0 +1,114 @@ +{ + "title": "tsconfig.json with TSTL", + "description": "JSON schema for the TypeScript compiler's configuration file with TSTL", + "$schema": "http://json-schema.org/draft-07/schema", + "allOf": [ + { + "$ref": "https://json.schemastore.org/tsconfig" + } + ], + "properties": { + "tstl": { + "description": "TypeScriptToLua compiler options.", + "type": "object", + "definitions": { + "//": { + "reference": "https://typescripttolua.github.io/docs/configuration#custom-options" + } + }, + "properties": { + "buildMode": { + "description": "Use buildMode: \"library\" to build publishable library packages.", + "type": "string", + "default": "library", + "enum": ["default", "library"] + }, + "extension": { + "description": "File extension for the resulting Lua files. Defaults to \".lua\"", + "type": "string" + }, + "lua51AllowTryCatchInAsyncAwait": { + "description": "Disable the warning that try/catch is not allowed in async functions in Lua 5.1, in case you are using a patched 5.1 lua version that supports this.", + "type": "boolean", + "default": false + }, + "luaBundle": { + "description": "The name of the lua file to bundle output lua to. Requires luaBundleEntry.", + "type": "string" + }, + "luaBundleEntry": { + "description": "The entry *.ts file that will be executed when entering the luaBundle. Requires luaBundle.", + "type": "string" + }, + "luaLibImport": { + "description": "Specifies how js standard features missing in lua are imported.", + "type": "string", + "default": "require", + "enum": ["none", "inline", "require", "require-minimal"] + }, + "luaTarget": { + "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", "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.", + "type": "boolean", + "default": false + }, + "noImplicitSelf": { + "description": "If true, treats all project files as if they were prefixed with\n/** @noSelfInFile **/.", + "type": "boolean", + "default": false + }, + "noHeader": { + "description": "Specify if a header will be added to compiled files.", + "type": "boolean", + "default": false + }, + "noResolvePaths": { + "description": "An array of import paths that should not be resolved but copied verbatim to output lua.", + "type": "array" + }, + "sourceMapTraceback": { + "description": "Applies the source map to show source TS files and lines in error tracebacks.", + "default": false, + "type": "boolean" + }, + "tstlVerbose": { + "description": "Give verbose tstl output, helpful when diagnosing tstl issues.", + "type": "boolean", + "default": false + }, + "luaPlugins": { + "description": "List of TypeScriptToLua plugins.", + "type": "array", + "items": { + "description": "Describes TypeScriptToLua plugin", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "Path to the JS file, that contains the plugin code", + "type": "string" + }, + "import": { + "type": "string" + } + } + } + }, + "measurePerformance": { + "description": "Measure and report performance of the tstl compiler.", + "type": "boolean" + } + }, + "dependencies": { + "luaBundle": ["luaBundleEntry"], + "luaBundleEntry": ["luaBundle"] + } + } + }, + "allowTrailingCommas": true +} diff --git a/tsconfig.json b/tsconfig.json index f214f1bed..bf121bd20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,21 @@ { "compilerOptions": { - "noImplicitThis" : true, - "alwaysStrict" : true, - "outDir": "./dist/", - "rootDir": "./src/", + "target": "es2019", + "lib": ["es2019"], + "types": ["node"], + "module": "commonjs", + "experimentalDecorators": true, + + "rootDir": "src", + "outDir": "dist", "declaration": true, "sourceMap": true, - "experimentalDecorators": true, - "target": "es2017", - "module": "commonjs" + "newLine": "LF", + "stripInternal": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": ["src"], "exclude": ["src/lualib"] diff --git a/tslint.json b/tslint.json deleted file mode 100644 index c321344c9..000000000 --- a/tslint.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "defaultSeverity": "error", - "rules": { - "array-type": [true, "array-simple"], - "arrow-parens": [true, "ban-single-arg-parens"], - "arrow-return-shorthand": true, - "ban": [true, - {"name": "parseInt", "message": "tsstyle#type-coercion"}, - {"name": "parseFloat", "message": "tsstyle#type-coercion"}, - {"name": "Array", "message": "tsstyle#array-constructor"} - ], - "ban-types": [true, - ["Object", "Use {} instead."], - ["String", "Use 'string' instead."], - ["Number", "Use 'number' instead."], - ["Boolean", "Use 'boolean' instead."] - ], - "class-name": true, - "curly": [true, "ignore-same-line"], - "deprecation": true, - "forin": false, - "import-spacing": true, - "interface-name": [true, "never-prefix"], - "jsdoc-format": true, - "label-position": true, - "max-classes-per-file": [true, 1], - "max-line-length": [true, 120], - "member-access": true, - "new-parens": true, - "no-angle-bracket-type-assertion": true, - "no-any": false, - "no-arg": true, - "no-conditional-assignment": true, - "no-construct": true, - "no-debugger": true, - "no-default-export": true, - "no-duplicate-variable": true, - "no-inferrable-types": true, - "no-namespace": [true, "allow-declarations"], - "no-null-keyword": true, - "no-reference": true, - "no-string-throw": true, - "no-trailing-whitespace": true, - "no-unused-expression": true, - "no-var-keyword": true, - "object-literal-shorthand": true, - "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], - "prefer-const": true, - "radix": true, - "semicolon": [true, "always", "ignore-bound-class-methods"], - "switch-default": false, - "trailing-comma": [ - true, - { - "multiline": { - "objects": "always", - "arrays": "always", - "functions": "never", - "typeLiterals": "always" - }, - "esSpecCompliant": true - } - ], - "triple-equals": [true, "allow-null-check"], - "typedef": [ - true, - "call-signature", - "property-declaration" - ], - "use-isnan": true, - "variable-name": [ - true, - "check-format", - "ban-keywords", - "allow-leading-underscore", - "allow-pascal-case" - ], - "whitespace": [true, "check-type-operator", "check-decl", "check-rest-spread", "check-typecast"] - }, - "rulesDirectory": [] -}