diff --git a/.eslintrc.json b/.eslintrc.json index 9123b6f..5f74930 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,22 +1,20 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/naming-convention": "warn", - "@typescript-eslint/semi": "warn", - "curly": "warn", - "eqeqeq": "warn", - "no-throw-literal": "warn", - "semi": "off" - }, - "ignorePatterns": [ - "**/*.d.ts" - ] + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "@typescript-eslint/no-inferrable-types": "off" + }, + "ignorePatterns": ["**/*.d.ts"] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2bd8db..cb95643 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,12 @@ jobs: matrix: node-version: ['18'] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run compile - - run: npm test + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run lint + - run: npm run check-format + - run: npm run compile + - run: npm test diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..aea847c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a55e556..23ec1bb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,8 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "dbaeumer.vscode-eslint", - "connor4312.esbuild-problem-matchers" - ] + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint", + "connor4312.esbuild-problem-matchers" + ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 18d4b9f..628505b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,56 +1,43 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "${workspaceFolder}/sampleWorkspace" - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], - "preLaunchTask": "npm: watch" - }, - { - "name": "Server", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}", - "program": "${workspaceFolder}/src/debugAdapter.ts", - "args": [ - "--server=4711" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: compile" - }, - { - "name": "Tests", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "-u", "tdd", - "--timeout", "999999", - "--colors", - "./out/tests/" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "internalConsoleOptions": "openOnSessionStart", - "preLaunchTask": "npm: compile" - } - ], - "compounds": [ - { - "name": "Extension + Server", - "configurations": [ "Extension", "Server" ] - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "${workspaceFolder}/sampleWorkspace" + ], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "npm: watch" + }, + { + "name": "Server", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/src/debugAdapter.ts", + "args": ["--server=4711"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: compile" + }, + { + "name": "Tests", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": ["-u", "tdd", "--timeout", "999999", "--colors", "./out/tests/"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "npm: compile" + } + ], + "compounds": [ + { + "name": "Extension + Server", + "configurations": ["Extension", "Server"] + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ed935b..f85186b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,17 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", - "git.branchProtection": ["main"], - "git.branchProtectionPrompt": "alwaysCommitToNewBranch" -} \ No newline at end of file + "git.branchProtection": ["main"], + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", + + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6bedf41..0d057e5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,32 +1,32 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "build", - "isBackground": false, - "group": { - "isDefault": true, - "kind": "build" - }, - "problemMatcher": "$esbuild", - "label": "npm: build" - }, - { - "type": "npm", - "script": "watch", - "group": "build", - "problemMatcher": "$esbuild-watch", - "isBackground": true, - "label": "npm: watch" - }, - { - "type": "npm", - "script": "watch-web", - "group": "build", - "problemMatcher": "$esbuild-watch", - "isBackground": true, - "label": "npm: watch web" - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "isBackground": false, + "group": { + "isDefault": true, + "kind": "build" + }, + "problemMatcher": "$esbuild", + "label": "npm: build" + }, + { + "type": "npm", + "script": "watch", + "group": "build", + "problemMatcher": "$esbuild-watch", + "isBackground": true, + "label": "npm: watch" + }, + { + "type": "npm", + "script": "watch-web", + "group": "build", + "problemMatcher": "$esbuild-watch", + "isBackground": true, + "label": "npm: watch web" + } + ] +} diff --git a/package-lock.json b/package-lock.json index edcf106..47f81a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,23 +14,27 @@ "lodash": "^4.17.21" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/glob": "^7.2.0", "@types/lodash": "^4.14.196", "@types/mocha": "^9.1.0", "@types/node": "^14.14.37", "@types/vscode": "^1.66.0", - "@typescript-eslint/eslint-plugin": "^5.17.0", - "@typescript-eslint/parser": "^5.17.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "@vscode/debugadapter": "^1.56.0", "@vscode/debugadapter-testsupport": "^1.56.0", "await-notify": "^1.0.1", "base64-js": "^1.5.1", "esbuild": "^0.14.29", - "eslint": "^8.12.0", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", "events": "^3.3.0", "glob": "^7.2.0", "mocha": "^10.2.0", + "nyc": "^15.1.0", "path-browserify": "^1.0.1", + "prettier": "^3.0.3", "rimraf": "^3.0.2", "ts-mocha": "^10.0.0", "typescript": "^4.6.3", @@ -50,6 +54,509 @@ "node": ">=0.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "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" + } + }, + "node_modules/@babel/core/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 + }, + "node_modules/@babel/core/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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -75,9 +582,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -98,21 +605,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -134,11 +641,191 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "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, + "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" + } + }, + "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, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "engines": { + "node": ">=6" + } + }, + "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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "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", @@ -421,6 +1108,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vscode/debugadapter": { "version": "1.61.0", "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.61.0.tgz", @@ -452,9 +1145,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -472,6 +1165,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -560,6 +1266,24 @@ "node": ">= 8" } }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -726,6 +1450,38 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "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" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -741,6 +1497,21 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -775,6 +1546,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001559", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz", + "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==", + "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" + } + ] + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -874,6 +1665,15 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -912,12 +1712,24 @@ "node": ">= 6" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, "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 }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1019,6 +1831,30 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-require-extensions/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, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", @@ -1116,6 +1952,12 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/electron-to-chromium": { + "version": "1.4.571", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.571.tgz", + "integrity": "sha512-Sc+VtKwKCDj3f/kLBjdyjMpNzoZsU6WuL/wFb6EH8USmHEcebxRXcRrVpOpayxd52tuey4RUDpUsw5OS5LhJqg==", + "dev": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1143,6 +1985,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/esbuild": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", @@ -1217,27 +2065,28 @@ } }, "node_modules/eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1270,6 +2119,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -1284,9 +2145,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "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, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1296,9 +2157,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", - "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -1337,6 +2198,19 @@ "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, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -1503,6 +2377,23 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1547,6 +2438,39 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "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" + } + ] + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -1565,6 +2489,15 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "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, + "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", @@ -1589,6 +2522,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1628,9 +2570,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1662,6 +2604,12 @@ "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==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -1713,6 +2661,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1739,6 +2712,12 @@ "node": ">=10" } }, + "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 + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -1812,6 +2791,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1842,72 +2830,196 @@ "dependencies": { "binary-extensions": "^2.0.0" }, - "engines": { - "node": ">=8" + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { "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==", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/is-glob": { + "node_modules/istanbul-lib-instrument": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "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==", + "node_modules/istanbul-lib-instrument/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, - "engines": { - "node": ">=0.12.0" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, "engines": { "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "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, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/istanbul-lib-report/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, + "dependencies": { + "semver": "^7.5.3" + }, "engines": { "node": ">=10" }, @@ -1915,11 +3027,32 @@ "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 + "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, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/js-sha256": { "version": "0.9.0", @@ -1936,6 +3069,12 @@ "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" }, + "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 + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1948,6 +3087,18 @@ "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, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -2043,6 +3194,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2077,6 +3234,30 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/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, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2298,100 +3479,304 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/mocha/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==", + "node_modules/mocha/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "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 + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/node-abi": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", + "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/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, + "engines": { + "node": ">=6" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc/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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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, "dependencies": { - "has-flag": "^4.0.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "node_modules/nyc/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, - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "p-limit": "^2.2.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=8" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "dev": true - }, - "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 - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true + "node_modules/nyc/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, + "engines": { + "node": ">=8" + } }, - "node_modules/node-abi": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz", - "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==", + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "dependencies": { - "semver": "^7.3.5" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, - "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==", + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "dependencies": { - "boolbase": "^1.0.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "engines": { + "node": ">=6" } }, "node_modules/object-inspect": { @@ -2459,6 +3844,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2562,6 +3983,12 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2574,6 +4001,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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, + "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, + "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, + "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, + "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, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -2609,6 +4100,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -2620,9 +4138,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -2720,6 +4238,18 @@ "node": ">=8.10.0" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2729,6 +4259,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2822,6 +4358,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2857,6 +4399,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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 + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2930,6 +4478,29 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3041,6 +4612,20 @@ "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, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3059,6 +4644,15 @@ "node": ">=8.17.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3218,6 +4812,15 @@ "underscore": "^1.12.1" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -3243,6 +4846,36 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "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" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "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", @@ -3280,6 +4913,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vlq": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz", @@ -3415,6 +5057,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -3444,6 +5092,18 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", diff --git a/package.json b/package.json index efad6e9..404d6c2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "compile": "tsc -p ./", "lint": "eslint src --ext ts", "typecheck": "tsc -p tsconfig.json --noEmit", + "check-format": "prettier . --check", + "format": "prettier . --write", "esbuild-base": "esbuild ./src/extension.ts --bundle --tsconfig=./tsconfig.json --external:vscode --format=cjs --platform=node --outfile=dist/extension.js", "watch": "npm run -S esbuild-base -- --sourcemap --sources-content=false --watch", "build": "npm run -S esbuild-base -- --sourcemap --sources-content=false", @@ -36,26 +38,49 @@ "publish": "vsce publish", "publish-pre-release": "vsce publish --pre-release", "vscode:prepublish": "rimraf dist && npm run -S esbuild-base -- --minify", - "test": "ts-mocha -p tsconfig.json tests/*test.ts --timeout 30s --diff false" + "test": "ts-mocha -p tsconfig.json tests/*test.ts --timeout 30s --diff false", + "test:coverage": "nyc npm run test" + }, + "nyc": { + "extends": "@istanbuljs/nyc-config-typescript", + "check-coverage": true, + "all": true, + "include": [ + "src/**/!(*.test.*).[tj]s?(x)" + ], + "exclude": [ + "src/_tests_/**/*.*" + ], + "reporter": [ + "html", + "lcov", + "text", + "text-summary" + ], + "report-dir": "coverage" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/glob": "^7.2.0", "@types/lodash": "^4.14.196", "@types/mocha": "^9.1.0", "@types/node": "^14.14.37", "@types/vscode": "^1.66.0", - "@typescript-eslint/eslint-plugin": "^5.17.0", - "@typescript-eslint/parser": "^5.17.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "@vscode/debugadapter": "^1.56.0", "@vscode/debugadapter-testsupport": "^1.56.0", "await-notify": "^1.0.1", "base64-js": "^1.5.1", "esbuild": "^0.14.29", - "eslint": "^8.12.0", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", "events": "^3.3.0", "glob": "^7.2.0", "mocha": "^10.2.0", + "nyc": "^15.1.0", "path-browserify": "^1.0.1", + "prettier": "^3.0.3", "rimraf": "^3.0.2", "ts-mocha": "^10.0.0", "typescript": "^4.6.3", diff --git a/sampleWorkspace/.vscode/launch.json b/sampleWorkspace/.vscode/launch.json index 1b2a438..f8aeabf 100644 --- a/sampleWorkspace/.vscode/launch.json +++ b/sampleWorkspace/.vscode/launch.json @@ -1,21 +1,25 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "teal", - "request": "launch", - "name": "Debug Transaction Group", - "debuggerPath": "stuff-to-deal", + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "teal", + "request": "launch", + "name": "Debug Transaction Group", + "debuggerPath": "stuff-to-deal", - "simulationTraceFile": "${workspaceFolder}/slot-machine/simulate-response.json", - "appSourceDescriptionFile": "${workspaceFolder}/slot-machine/sources.json", - "program": "${workspaceFolder}/stack-scratch.teal", + // "program": "${workspaceFolder}/stepping-test/simulate-response.json", + // "simulationTraceFile": "${workspaceFolder}/stepping-test/simulate-response.json", + // "appSourceDescriptionFile": "${workspaceFolder}/stepping-test/sources.json", - "stopOnEntry": true, - "trace": false - } - ] + "program": "${workspaceFolder}/slot-machine/simulate-response.json", + "simulationTraceFile": "${workspaceFolder}/slot-machine/simulate-response.json", + "appSourceDescriptionFile": "${workspaceFolder}/slot-machine/sources.json", + + "stopOnEntry": true, + "trace": false + } + ] } diff --git a/sampleWorkspace/app-state-changes/box-simulate-response.json b/sampleWorkspace/app-state-changes/box-simulate-response.json index 8f8e021..94edb75 100644 --- a/sampleWorkspace/app-state-changes/box-simulate-response.json +++ b/sampleWorkspace/app-state-changes/box-simulate-response.json @@ -9,9 +9,7 @@ "txn": { "sig": "t9sBFT0TnKRnXz61oyYvW4XL9ngK67ciWUEIRl5syDun4N7bqiOta+Sub02OmzcusmChEMf9NOn8SODAWvsNBA==", "txn": { - "apaa": [ - "1RPETg==" - ], + "apaa": ["1RPETg=="], "apbx": [ { "n": "Ym94LWtleS0x" @@ -216,4 +214,4 @@ } ] } -} \ No newline at end of file +} diff --git a/sampleWorkspace/app-state-changes/global-simulate-response.json b/sampleWorkspace/app-state-changes/global-simulate-response.json index 6167fff..eae878b 100644 --- a/sampleWorkspace/app-state-changes/global-simulate-response.json +++ b/sampleWorkspace/app-state-changes/global-simulate-response.json @@ -9,9 +9,7 @@ "txn": { "sig": "9Sm0GLdwLZ8a8VWVZXFgdTnk4Bc8Gsoq5Zkc1BKUAKBMuoZRuiD91I3AP8poj8lg5/tdecZuXvaPuaSocXpiBQ==", "txn": { - "apaa": [ - "iRPB+A==" - ], + "apaa": ["iRPB+A=="], "apbx": [ { "n": "Ym94LWtleS0x" @@ -237,4 +235,4 @@ } ] } -} \ No newline at end of file +} diff --git a/sampleWorkspace/app-state-changes/local-simulate-response.json b/sampleWorkspace/app-state-changes/local-simulate-response.json index 899cb24..d62cd01 100644 --- a/sampleWorkspace/app-state-changes/local-simulate-response.json +++ b/sampleWorkspace/app-state-changes/local-simulate-response.json @@ -9,9 +9,7 @@ "txn": { "sig": "6c6pQryUHynVHKK0/3uTiqJIiiJLu+4xbSAZ7YOIZK8+Whve/2BOfrznrV4WYS4tOUCL0MyTA5d1gH5jeSnFBA==", "txn": { - "apaa": [ - "jhaTEQ==" - ], + "apaa": ["jhaTEQ=="], "apbx": [ { "n": "Ym94LWtleS0x" @@ -261,4 +259,4 @@ } ] } -} \ No newline at end of file +} diff --git a/sampleWorkspace/app-state-changes/sources.json b/sampleWorkspace/app-state-changes/sources.json index 8108c03..f24d278 100644 --- a/sampleWorkspace/app-state-changes/sources.json +++ b/sampleWorkspace/app-state-changes/sources.json @@ -1,8 +1,8 @@ { - "txn-group-sources": [ - { - "sourcemap-location": "./state-changes.teal.tok.map", - "hash": "elIoqp1XgWrLCBLPmaZlDsKE3sEMZBY1dlxOvBXPtak=" - } - ] + "txn-group-sources": [ + { + "sourcemap-location": "./state-changes.teal.tok.map", + "hash": "elIoqp1XgWrLCBLPmaZlDsKE3sEMZBY1dlxOvBXPtak=" + } + ] } diff --git a/sampleWorkspace/recursive-app/simulate-response.json b/sampleWorkspace/recursive-app/simulate-response.json index 921d3cf..165b39d 100644 --- a/sampleWorkspace/recursive-app/simulate-response.json +++ b/sampleWorkspace/recursive-app/simulate-response.json @@ -435,9 +435,7 @@ }, { "pc": 89, - "spawned-inners": [ - 0 - ] + "spawned-inners": [0] }, { "pc": 90 @@ -659,10 +657,7 @@ }, { "pc": 145, - "spawned-inners": [ - 1, - 2 - ] + "spawned-inners": [1, 2] }, { "pc": 146 @@ -1149,9 +1144,7 @@ }, { "pc": 89, - "spawned-inners": [ - 0 - ] + "spawned-inners": [0] }, { "pc": 90 @@ -1372,10 +1365,7 @@ }, { "pc": 145, - "spawned-inners": [ - 1, - 2 - ] + "spawned-inners": [1, 2] }, { "pc": 146 @@ -1889,15 +1879,11 @@ } }, { - "logs": [ - "AAAAAAAAAAE=" - ], + "logs": ["AAAAAAAAAAE="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "AAAAAAAAAAA=" - ], + "apaa": ["AAAAAAAAAAA="], "apan": 5, "apid": 1007, "fv": 4, @@ -1909,15 +1895,11 @@ } } ], - "logs": [ - "AAAAAAAAAAI=" - ], + "logs": ["AAAAAAAAAAI="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "AAAAAAAAAAE=" - ], + "apaa": ["AAAAAAAAAAE="], "apan": 5, "apid": 1004, "fv": 4, @@ -1929,16 +1911,12 @@ } } ], - "logs": [ - "AAAAAAAAAAQ=" - ], + "logs": ["AAAAAAAAAAQ="], "pool-error": "", "txn": { "sig": "UxR2wgq7x1XCxPoO3cqXDw9X9xV3CNC3FWziAbN+m8QKNxG+XwLUyDrwQ5VGgporwLsqsTTVAQvsa0WAPKcrBg==", "txn": { - "apaa": [ - "AAAAAAAAAAI=" - ], + "apaa": ["AAAAAAAAAAI="], "apid": 1001, "fee": 8000, "fv": 4, @@ -1955,4 +1933,4 @@ } ], "version": 2 -} \ No newline at end of file +} diff --git a/sampleWorkspace/recursive-app/sources.json b/sampleWorkspace/recursive-app/sources.json index e2b6d17..ebe3acb 100644 --- a/sampleWorkspace/recursive-app/sources.json +++ b/sampleWorkspace/recursive-app/sources.json @@ -1,8 +1,8 @@ { - "txn-group-sources": [ - { - "sourcemap-location": "./recursive-inner-approval.teal.tok.map", - "hash": "udcjx6hXtg7Jw3QgsiuatawbeyefdwVgyRhZon6tu1A=" - } - ] -} \ No newline at end of file + "txn-group-sources": [ + { + "sourcemap-location": "./recursive-inner-approval.teal.tok.map", + "hash": "udcjx6hXtg7Jw3QgsiuatawbeyefdwVgyRhZon6tu1A=" + } + ] +} diff --git a/sampleWorkspace/slot-machine/simulate-response.json b/sampleWorkspace/slot-machine/simulate-response.json index e5de921..38ac2de 100644 --- a/sampleWorkspace/slot-machine/simulate-response.json +++ b/sampleWorkspace/slot-machine/simulate-response.json @@ -927,11 +927,7 @@ }, { "pc": 108, - "spawned-inners": [ - 0, - 1, - 2 - ] + "spawned-inners": [0, 1, 2] }, { "pc": 109, @@ -2110,9 +2106,7 @@ }, { "pc": 45, - "spawned-inners": [ - 0 - ] + "spawned-inners": [0] }, { "pc": 46, @@ -2847,9 +2841,7 @@ }, { "pc": 45, - "spawned-inners": [ - 0 - ] + "spawned-inners": [0] }, { "pc": 46, @@ -3590,9 +3582,7 @@ }, { "pc": 45, - "spawned-inners": [ - 0 - ] + "spawned-inners": [0] }, { "pc": 46, @@ -4205,16 +4195,11 @@ } } ], - "logs": [ - "FR98dQAAAAAAAAAAAAAAAAH6X10jAAAAAAAAAAA=" - ], + "logs": ["FR98dQAAAAAAAAAAAAAAAAH6X10jAAAAAAAAAAA="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "v4rK7g==", - "AAAAAAAAAAg=" - ], + "apaa": ["v4rK7g==", "AAAAAAAAAAg="], "apid": 82873991, "fee": 1000, "fv": 33185554, @@ -4225,19 +4210,12 @@ } } ], - "logs": [ - "FR98dUAAAAAAAfpfXSMAAAAAAAAAAA==" - ], + "logs": ["FR98dUAAAAAAAfpfXSMAAAAAAAAAAA=="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "JCa9/A==", - "AAhAIS0tLS0tLQ==" - ], - "apfa": [ - 82873991 - ], + "apaa": ["JCa9/A==", "AAhAIS0tLS0tLQ=="], + "apfa": [82873991], "apid": 82874344, "fee": 1000, "fv": 33185554, @@ -4260,16 +4238,11 @@ } } ], - "logs": [ - "FR98dQAAAAAAAAABAAAAAAH6X10jAAAAAAAAAAE=" - ], + "logs": ["FR98dQAAAAAAAAABAAAAAAH6X10jAAAAAAAAAAE="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "v4rK7g==", - "AAAAAAAAAAY=" - ], + "apaa": ["v4rK7g==", "AAAAAAAAAAY="], "apid": 82873991, "fee": 1000, "fv": 33185554, @@ -4280,19 +4253,12 @@ } } ], - "logs": [ - "FR98dUAAAAAAAfpfXSMAAAAAAAAAAQ==" - ], + "logs": ["FR98dUAAAAAAAfpfXSMAAAAAAAAAAQ=="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "JCa9/A==", - "AAZAQCEtLS0=" - ], - "apfa": [ - 82873991 - ], + "apaa": ["JCa9/A==", "AAZAQCEtLS0="], + "apfa": [82873991], "apid": 82874344, "fee": 1000, "fv": 33185554, @@ -4315,16 +4281,11 @@ } } ], - "logs": [ - "FR98dQAAAAAAAAACAAAAAAH6X10jAAAAAAAAAAI=" - ], + "logs": ["FR98dQAAAAAAAAACAAAAAAH6X10jAAAAAAAAAAI="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "v4rK7g==", - "AAAAAAAAAAM=" - ], + "apaa": ["v4rK7g==", "AAAAAAAAAAM="], "apid": 82873991, "fee": 1000, "fv": 33185554, @@ -4335,19 +4296,12 @@ } } ], - "logs": [ - "FR98dS0AAAAAAfpfXSMAAAAAAAAAAg==" - ], + "logs": ["FR98dS0AAAAAAfpfXSMAAAAAAAAAAg=="], "pool-error": "", "txn": { "txn": { - "apaa": [ - "JCa9/A==", - "AANAIS0=" - ], - "apfa": [ - 82873991 - ], + "apaa": ["JCa9/A==", "AANAIS0="], + "apfa": [82873991], "apid": 82874344, "fee": 1000, "fv": 33185554, @@ -4378,15 +4332,8 @@ "txn": { "sig": "JzNmfWIVrRO1QfPyJoKVA1Ovq+IaZRbp9wSVAdVUgviyrxgzjuSQqSngz2j/DoFpxD+nUjSIK8olIiFs0YQFCA==", "txn": { - "apaa": [ - "aXydOw==", - "AQ==", - "Ag==" - ], - "apfa": [ - 82873991, - 82874344 - ], + "apaa": ["aXydOw==", "AQ==", "Ag=="], + "apfa": [82873991, 82874344], "apid": 82874798, "fee": 1000, "fv": 33185554, diff --git a/sampleWorkspace/slot-machine/sources.json b/sampleWorkspace/slot-machine/sources.json index 94133bb..0b7abaa 100644 --- a/sampleWorkspace/slot-machine/sources.json +++ b/sampleWorkspace/slot-machine/sources.json @@ -1,16 +1,16 @@ { - "txn-group-sources": [ - { - "sourcemap-location": "./slot-machine.teal.tok.map", - "hash": "AYqVv/i0Y88cebp0m1MAYE9gk/1SnhFm8oLKOBMOKac=" - }, - { - "sourcemap-location": "./random-byte.teal.tok.map", - "hash": "nN5LNX4rlRXfN0ax9hH0TsYh1XDhOSLnANPnrWSdATw=" - }, - { - "sourcemap-location": "./fake-random.teal.tok.map", - "hash": "88rrCGDJdARk4rasK5FN8BzmKTqHW5WRzYiRWmtA7HY=" - } - ] -} \ No newline at end of file + "txn-group-sources": [ + { + "sourcemap-location": "./slot-machine.teal.tok.map", + "hash": "AYqVv/i0Y88cebp0m1MAYE9gk/1SnhFm8oLKOBMOKac=" + }, + { + "sourcemap-location": "./random-byte.teal.tok.map", + "hash": "nN5LNX4rlRXfN0ax9hH0TsYh1XDhOSLnANPnrWSdATw=" + }, + { + "sourcemap-location": "./fake-random.teal.tok.map", + "hash": "88rrCGDJdARk4rasK5FN8BzmKTqHW5WRzYiRWmtA7HY=" + } + ] +} diff --git a/sampleWorkspace/sourcemap-test/sources.json b/sampleWorkspace/sourcemap-test/sources.json index e87a7ae..171fbe8 100644 --- a/sampleWorkspace/sourcemap-test/sources.json +++ b/sampleWorkspace/sourcemap-test/sources.json @@ -1,8 +1,8 @@ { - "txn-group-sources": [ - { - "sourcemap-location": "./sourcemap-test.teal.tok.map", - "hash": "7brAsOgat+0ZJIS07Pl3ZMG+5+u/A6g0+pUiNNvVCg0=" - } - ] + "txn-group-sources": [ + { + "sourcemap-location": "./sourcemap-test.teal.tok.map", + "hash": "7brAsOgat+0ZJIS07Pl3ZMG+5+u/A6g0+pUiNNvVCg0=" + } + ] } diff --git a/sampleWorkspace/stack-scratch/sources.json b/sampleWorkspace/stack-scratch/sources.json index 3d7bfa1..7b70342 100644 --- a/sampleWorkspace/stack-scratch/sources.json +++ b/sampleWorkspace/stack-scratch/sources.json @@ -1,8 +1,8 @@ { - "txn-group-sources": [ - { - "sourcemap-location": "./stack-scratch.teal.tok.map", - "hash": "3QzcaT21ZU72t0ZjW0Bq4l2/3zK4aw7NL05Q/43i7sg=" - } - ] -} \ No newline at end of file + "txn-group-sources": [ + { + "sourcemap-location": "./stack-scratch.teal.tok.map", + "hash": "3QzcaT21ZU72t0ZjW0Bq4l2/3zK4aw7NL05Q/43i7sg=" + } + ] +} diff --git a/sampleWorkspace/stepping-test/app.teal b/sampleWorkspace/stepping-test/app.teal new file mode 100644 index 0000000..9af28ce --- /dev/null +++ b/sampleWorkspace/stepping-test/app.teal @@ -0,0 +1,14 @@ +#pragma version 9 +txn ApplicationID +bz end + +callsub main + +end: +int 1 +return + +main: +txn Sender +log +retsub diff --git a/sampleWorkspace/stepping-test/app.teal.tok.map b/sampleWorkspace/stepping-test/app.teal.tok.map new file mode 100644 index 0000000..06b93c8 --- /dev/null +++ b/sampleWorkspace/stepping-test/app.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["app.teal"],"names":[],"mappings":";AACA;;AACA;;;AAEA;;;AAGA;;AACA;AAGA;;AACA;AACA"} \ No newline at end of file diff --git a/sampleWorkspace/stepping-test/lsig.teal b/sampleWorkspace/stepping-test/lsig.teal new file mode 100644 index 0000000..6fc4b6b --- /dev/null +++ b/sampleWorkspace/stepping-test/lsig.teal @@ -0,0 +1,7 @@ +#pragma version 9 +txn Fee +! +txn RekeyTo +global ZeroAddress +== +&& diff --git a/sampleWorkspace/stepping-test/lsig.teal.tok.map b/sampleWorkspace/stepping-test/lsig.teal.tok.map new file mode 100644 index 0000000..63cdf07 --- /dev/null +++ b/sampleWorkspace/stepping-test/lsig.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["lsig.teal"],"names":[],"mappings":";AACA;;AACA;AACA;;AACA;;AACA;AACA"} \ No newline at end of file diff --git a/sampleWorkspace/stepping-test/nine.teal b/sampleWorkspace/stepping-test/nine.teal new file mode 100644 index 0000000..e211251 --- /dev/null +++ b/sampleWorkspace/stepping-test/nine.teal @@ -0,0 +1,2 @@ +#pragma version 9 +int 1 diff --git a/sampleWorkspace/stepping-test/nine.teal.tok.map b/sampleWorkspace/stepping-test/nine.teal.tok.map new file mode 100644 index 0000000..e2ba07f --- /dev/null +++ b/sampleWorkspace/stepping-test/nine.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["nine.teal"],"names":[],"mappings":";AACA"} \ No newline at end of file diff --git a/sampleWorkspace/stepping-test/simulate-response.json b/sampleWorkspace/stepping-test/simulate-response.json new file mode 100644 index 0000000..2111595 --- /dev/null +++ b/sampleWorkspace/stepping-test/simulate-response.json @@ -0,0 +1,260 @@ +{ + "exec-trace-config": { + "enable": true, + "scratch-change": true, + "stack-change": true, + "state-change": true + }, + "last-round": 1, + "txn-groups": [ + { + "app-budget-added": 700, + "app-budget-consumed": 8, + "txn-results": [ + { + "txn-result": { + "pool-error": "", + "txn": { + "sig": "NQwSCPKxmu3+k7xsRP30KOTybJs7VEleHUaY2Dcu8mvVdqxu5gxuzpdRb2SmWlsbHYoJkvAR19NY8Ad+rPS3DQ==", + "txn": { + "amt": 1000000, + "fee": 3000, + "fv": 1, + "gen": "sandnet-v1", + "gh": "ifK2RQF+3sOKGHGatMPIZXys92Le0M24cPWHLZzQCMM=", + "grp": "tL7b6oV1s8p3EbAAgbFNW5SQ4z7vIoAY+74QK77sjrw=", + "lv": 1001, + "note": "FZmTzK+eq1E=", + "rcv": "XN3M2DLC2FVH3OK5ESENK3V7A5GG6INENDOKK5VIRQQJVMY7QU7DV6PXQA", + "snd": "KYHNDXENAGPPJWWQ5KNZRYQ6XQ7I6536WI4S3366MBUKP7RGAPOHBBK6MQ", + "type": "pay" + } + } + } + }, + { + "app-budget-consumed": 8, + "exec-trace": { + "approval-program-hash": "s5BW9MJiUZwls84EcgHuA9Hnbok6U83mg0VlW+QCBCE=", + "approval-program-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2, + "uint": 1001 + } + ] + }, + { + "pc": 3, + "stack-pop-count": 1 + }, + { + "pc": 6 + }, + { + "pc": 12, + "stack-additions": [ + { + "bytes": "UIFDtcFjZUCtZnj2emSqN+VMKx9MhZ2qvrVfsrFEfB0=", + "type": 1 + } + ] + }, + { + "pc": 14, + "stack-pop-count": 1 + }, + { + "pc": 15 + }, + { + "pc": 9, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ] + }, + { + "pc": 11, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 1 + } + ], + "logic-sig-hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=", + "logic-sig-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2 + } + ] + }, + { + "pc": 3, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 1 + }, + { + "pc": 4, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 6, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 8, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + }, + { + "pc": 9, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + } + ] + }, + "logic-sig-budget-consumed": 6, + "txn-result": { + "logs": ["UIFDtcFjZUCtZnj2emSqN+VMKx9MhZ2qvrVfsrFEfB0="], + "pool-error": "", + "txn": { + "lsig": { + "l": "CTEBFDEgMgMSEA==" + }, + "txn": { + "apid": 1001, + "fv": 1, + "gh": "ifK2RQF+3sOKGHGatMPIZXys92Le0M24cPWHLZzQCMM=", + "grp": "tL7b6oV1s8p3EbAAgbFNW5SQ4z7vIoAY+74QK77sjrw=", + "lv": 1001, + "note": "EEzHktFpJSY=", + "snd": "KCAUHNOBMNSUBLLGPD3HUZFKG7SUYKY7JSCZ3KV6WVP3FMKEPQOYS3KG5Y", + "type": "appl" + } + } + } + }, + { + "exec-trace": { + "logic-sig-hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=", + "logic-sig-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2 + } + ] + }, + { + "pc": 3, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 1 + }, + { + "pc": 4, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 6, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 8, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + }, + { + "pc": 9, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + } + ] + }, + "logic-sig-budget-consumed": 6, + "txn-result": { + "pool-error": "", + "txn": { + "lsig": { + "l": "CTEBFDEgMgMSEA==" + }, + "txn": { + "fv": 1, + "gen": "sandnet-v1", + "gh": "ifK2RQF+3sOKGHGatMPIZXys92Le0M24cPWHLZzQCMM=", + "grp": "tL7b6oV1s8p3EbAAgbFNW5SQ4z7vIoAY+74QK77sjrw=", + "lv": 1001, + "note": "g7OW8o9Wqlo=", + "rcv": "KCAUHNOBMNSUBLLGPD3HUZFKG7SUYKY7JSCZ3KV6WVP3FMKEPQOYS3KG5Y", + "snd": "KCAUHNOBMNSUBLLGPD3HUZFKG7SUYKY7JSCZ3KV6WVP3FMKEPQOYS3KG5Y", + "type": "pay" + } + } + } + } + ] + } + ], + "version": 2 +} diff --git a/sampleWorkspace/stepping-test/sources.json b/sampleWorkspace/stepping-test/sources.json new file mode 100644 index 0000000..8000140 --- /dev/null +++ b/sampleWorkspace/stepping-test/sources.json @@ -0,0 +1,12 @@ +{ + "txn-group-sources": [ + { + "sourcemap-location": "./lsig.teal.tok.map", + "hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=" + }, + { + "sourcemap-location": "./app.teal.tok.map", + "hash": "s5BW9MJiUZwls84EcgHuA9Hnbok6U83mg0VlW+QCBCE=" + } + ] +} diff --git a/src/activateMockDebug.ts b/src/activateMockDebug.ts index acdf753..89e336d 100644 --- a/src/activateMockDebug.ts +++ b/src/activateMockDebug.ts @@ -5,85 +5,109 @@ import { FileAccessor } from './debugAdapter/utils'; import { TEALDebugAdapterDescriptorFactory } from './extension'; import { TealDebugConfigProvider } from './configuration'; -export function activateTealDebug(context: vscode.ExtensionContext, factory: TEALDebugAdapterDescriptorFactory, config: vscode.DebugConfiguration) { +export function activateTealDebug( + context: vscode.ExtensionContext, + factory: TEALDebugAdapterDescriptorFactory, + config: vscode.DebugConfiguration, +) { + context.subscriptions.push( + vscode.commands.registerCommand( + 'extension.teal-debug.runEditorContents', + (resource: vscode.Uri) => { + let targetResource = resource; + if (!targetResource && vscode.window.activeTextEditor) { + targetResource = vscode.window.activeTextEditor.document.uri; + } + if (targetResource) { + vscode.debug.startDebugging( + undefined, + { + ...config, + name: 'Run File', + program: targetResource.fsPath, + }, + { noDebug: true }, + ); + } + }, + ), + vscode.commands.registerCommand( + 'extension.teal-debug.debugEditorContents', + (resource: vscode.Uri) => { + let targetResource = resource; + if (!targetResource && vscode.window.activeTextEditor) { + targetResource = vscode.window.activeTextEditor.document.uri; + } + if (targetResource) { + vscode.debug.startDebugging(undefined, { + ...config, + name: 'Debug File', + program: targetResource.fsPath, + }); + } + }, + ), + ); - context.subscriptions.push( - vscode.commands.registerCommand('extension.teal-debug.runEditorContents', (resource: vscode.Uri) => { - let targetResource = resource; - if (!targetResource && vscode.window.activeTextEditor) { - targetResource = vscode.window.activeTextEditor.document.uri; - } - if (targetResource) { - vscode.debug.startDebugging(undefined, { - ...config, - name: 'Run File', - program: targetResource.fsPath - }, - { noDebug: true } - ); - } - }), - vscode.commands.registerCommand('extension.teal-debug.debugEditorContents', (resource: vscode.Uri) => { - let targetResource = resource; - if (!targetResource && vscode.window.activeTextEditor) { - targetResource = vscode.window.activeTextEditor.document.uri; - } - if (targetResource) { - vscode.debug.startDebugging(undefined, { - ...config, - name: 'Debug File', - program: targetResource.fsPath - }); - } - }) - ); + // register a configuration provider for 'teal' debug type + const provider = new TealDebugConfigProvider(); + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider('teal', provider), + ); - // register a configuration provider for 'teal' debug type - const provider = new TealDebugConfigProvider(); - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('teal', provider)); + context.subscriptions.push( + vscode.debug.registerDebugAdapterDescriptorFactory('teal', factory), + ); + // https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/#events + // see events, by the end of subscription, call `dispose()` to release resource. + context.subscriptions.push(factory); - context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('teal', factory)); - // https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/#events - // see events, by the end of subscription, call `dispose()` to release resource. - context.subscriptions.push(factory); + // override VS Code's default implementation of the debug hover + // here we match only Mock "variables", that are words starting with an '$' + context.subscriptions.push( + vscode.languages.registerEvaluatableExpressionProvider('markdown', { + provideEvaluatableExpression( + document: vscode.TextDocument, + position: vscode.Position, + ): vscode.ProviderResult { + const VARIABLE_REGEXP = /\$[a-z][a-z0-9]*/gi; + const line = document.lineAt(position.line).text; - // override VS Code's default implementation of the debug hover - // here we match only Mock "variables", that are words starting with an '$' - context.subscriptions.push(vscode.languages.registerEvaluatableExpressionProvider('markdown', { - provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { + let m: RegExpExecArray | null; + while ((m = VARIABLE_REGEXP.exec(line))) { + const varRange = new vscode.Range( + position.line, + m.index, + position.line, + m.index + m[0].length, + ); - const VARIABLE_REGEXP = /\$[a-z][a-z0-9]*/ig; - const line = document.lineAt(position.line).text; - - let m: RegExpExecArray | null; - while (m = VARIABLE_REGEXP.exec(line)) { - const varRange = new vscode.Range(position.line, m.index, position.line, m.index + m[0].length); - - if (varRange.contains(position)) { - return new vscode.EvaluatableExpression(varRange); - } - } - return undefined; - } - })); + if (varRange.contains(position)) { + return new vscode.EvaluatableExpression(varRange); + } + } + return undefined; + }, + }), + ); } export const workspaceFileAccessor: FileAccessor = { - isWindows: typeof process !== 'undefined' && process.platform === 'win32', - async readFile(path: string): Promise { - const uri = pathToUri(path); - return await vscode.workspace.fs.readFile(uri); - }, - async writeFile(path: string, contents: Uint8Array) { - const uri = pathToUri(path); - await vscode.workspace.fs.writeFile(uri, contents); - } + isWindows: typeof process !== 'undefined' && process.platform === 'win32', + async readFile(path: string): Promise { + const uri = pathToUri(path); + return await vscode.workspace.fs.readFile(uri); + }, + async writeFile(path: string, contents: Uint8Array) { + const uri = pathToUri(path); + await vscode.workspace.fs.writeFile(uri, contents); + }, }; function pathToUri(path: string) { - try { - return vscode.Uri.file(path); - } catch (e) { - return vscode.Uri.parse(path, true); - } + try { + return vscode.Uri.file(path); + } catch (e) { + return vscode.Uri.parse(path, true); + } } diff --git a/src/configuration.ts b/src/configuration.ts index 512ad72..4388fb2 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,41 +1,49 @@ 'use strict'; import * as vscode from 'vscode'; -import { WorkspaceFolder, DebugConfiguration, ProviderResult, CancellationToken } from 'vscode'; +import { + WorkspaceFolder, + DebugConfiguration, + ProviderResult, + CancellationToken, +} from 'vscode'; export class TealDebugConfigProvider - implements vscode.DebugConfigurationProvider { + implements vscode.DebugConfigurationProvider +{ + /** + * Massage a debug configuration just before a debug session is being launched, + * e.g. add all missing attributes to the debug configuration. + */ + resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + config: DebugConfiguration, + _token?: CancellationToken, + ): ProviderResult { + // NOTE: log the overloaded config to window + vscode.window.showInformationMessage(JSON.stringify(config)); - /** - * Massage a debug configuration just before a debug session is being launched, - * e.g. add all missing attributes to the debug configuration. - */ - resolveDebugConfiguration( - _folder: WorkspaceFolder | undefined, - config: DebugConfiguration, - _token?: CancellationToken - ): ProviderResult { + // Check necessary part, we do need these 2 files for debug + if (!config.simulationTraceFile) { + return vscode.window + .showInformationMessage( + 'missing critical part: simulationTraceFile in launch.json', + ) + .then((_) => { + return undefined; + }); + } - // NOTE: log the overloaded config to window - vscode.window.showInformationMessage(JSON.stringify(config)); + if (!config.appSourceDescriptionFile) { + return vscode.window + .showInformationMessage( + 'missing critical part: appSourceDescriptionFile in launch.json', + ) + .then((_) => { + return undefined; + }); + } - // Check necessary part, we do need these 2 files for debug - if (!config.simulationTraceFile) { - return vscode.window.showInformationMessage( - 'missing critical part: simulationTraceFile in launch.json' - ).then(_ => { - return undefined; - }); - } - - if (!config.appSourceDescriptionFile) { - return vscode.window.showInformationMessage( - 'missing critical part: appSourceDescriptionFile in launch.json' - ).then(_ => { - return undefined; - }); - } - - return config; - } + return config; + } } diff --git a/src/debugAdapter/appState.ts b/src/debugAdapter/appState.ts index 9046bd7..114cd8f 100644 --- a/src/debugAdapter/appState.ts +++ b/src/debugAdapter/appState.ts @@ -1,72 +1,88 @@ -import * as algosdk from "algosdk"; -import { ByteArrayMap } from "./utils"; +import * as algosdk from 'algosdk'; +import { ByteArrayMap } from './utils'; export class AppState { - globalState: ByteArrayMap; - localState: Map>; - boxState: ByteArrayMap; + globalState: ByteArrayMap; + localState: Map>; + boxState: ByteArrayMap; - constructor() { - this.globalState = new ByteArrayMap(); - this.localState = new Map>(); - this.boxState = new ByteArrayMap(); - } + constructor() { + this.globalState = new ByteArrayMap(); + this.localState = new Map< + string, + ByteArrayMap + >(); + this.boxState = new ByteArrayMap(); + } - public globalStateArray(): algosdk.modelsv2.AvmKeyValue[] { - return createAvmKvArray(this.globalState); - } + public globalStateArray(): algosdk.modelsv2.AvmKeyValue[] { + return createAvmKvArray(this.globalState); + } - public localStateArray(account: string): algosdk.modelsv2.AvmKeyValue[] { - const map = this.localState.get(account); - if (!map) { - return []; - } - return createAvmKvArray(map); - } - - public boxStateArray(): algosdk.modelsv2.AvmKeyValue[] { - return createAvmKvArray(this.boxState); - } - - public clone(): AppState { - const clone = new AppState(); - clone.globalState = this.globalState.clone(); - clone.localState = new Map( - Array.from(this.localState.entries(), ([key, value]) => [key, value.clone()]) - ); - clone.boxState = this.boxState.clone(); - return clone; + public localStateArray(account: string): algosdk.modelsv2.AvmKeyValue[] { + const map = this.localState.get(account); + if (!map) { + return []; } + return createAvmKvArray(map); + } + + public boxStateArray(): algosdk.modelsv2.AvmKeyValue[] { + return createAvmKvArray(this.boxState); + } - public static fromAppInitialState(initialState: algosdk.modelsv2.ApplicationInitialStates): AppState { - const state = new AppState(); + public clone(): AppState { + const clone = new AppState(); + clone.globalState = this.globalState.clone(); + clone.localState = new Map( + Array.from(this.localState.entries(), ([key, value]) => [ + key, + value.clone(), + ]), + ); + clone.boxState = this.boxState.clone(); + return clone; + } - if (initialState.appGlobals) { - for (const { key, value } of initialState.appGlobals.kvs) { - state.globalState.set(key, value); - } - } + public static fromAppInitialState( + initialState: algosdk.modelsv2.ApplicationInitialStates, + ): AppState { + const state = new AppState(); - for (const appLocal of initialState.appLocals || []) { - const map = new ByteArrayMap(); - for (const { key, value } of appLocal.kvs) { - map.set(key, value); - } - state.localState.set(appLocal.account!, map); - } + if (initialState.appGlobals) { + for (const { key, value } of initialState.appGlobals.kvs) { + state.globalState.set(key, value); + } + } - if (initialState.appBoxes) { - for (const { key, value } of initialState.appBoxes.kvs) { - state.boxState.set(key, value); - } - } + for (const appLocal of initialState.appLocals || []) { + const map = new ByteArrayMap(); + for (const { key, value } of appLocal.kvs) { + map.set(key, value); + } + state.localState.set(appLocal.account!, map); + } - return state; + if (initialState.appBoxes) { + for (const { key, value } of initialState.appBoxes.kvs) { + state.boxState.set(key, value); + } } + + return state; + } } -function createAvmKvArray(map: ByteArrayMap): algosdk.modelsv2.AvmKeyValue[] { - return Array.from(map.entriesHex()) - .sort() - .map(([key, value]) => new algosdk.modelsv2.AvmKeyValue({ key: Buffer.from(key, 'hex'), value })); +function createAvmKvArray( + map: ByteArrayMap, +): algosdk.modelsv2.AvmKeyValue[] { + return Array.from(map.entriesHex()) + .sort() + .map( + ([key, value]) => + new algosdk.modelsv2.AvmKeyValue({ + key: Buffer.from(key, 'hex'), + value, + }), + ); } diff --git a/src/debugAdapter/basicServer.ts b/src/debugAdapter/basicServer.ts index aa88bbc..7835f53 100644 --- a/src/debugAdapter/basicServer.ts +++ b/src/debugAdapter/basicServer.ts @@ -3,30 +3,29 @@ import { TxnGroupDebugSession } from './debugRequestHandlers'; import { FileAccessor, TEALDebuggingAssets } from './utils'; export class BasicServer { + private server: Net.Server; - private server: Net.Server; + constructor(fileAccessor: FileAccessor, debugAssets: TEALDebuggingAssets) { + this.server = Net.createServer((socket) => { + const session = new TxnGroupDebugSession(fileAccessor, debugAssets); + session.setRunAsServer(true); + session.start(socket as NodeJS.ReadableStream, socket); + socket.on('error', (err) => { + throw err; + }); + }).listen(0); + this.server.on('error', (err) => { + throw err; + }); + } - constructor(fileAccessor: FileAccessor, debugAssets: TEALDebuggingAssets) { - this.server = Net.createServer(socket => { - const session = new TxnGroupDebugSession(fileAccessor, debugAssets); - session.setRunAsServer(true); - session.start(socket as NodeJS.ReadableStream, socket); - socket.on('error', (err) => { - throw err; - }); - }).listen(0); - this.server.on('error', (err) => { - throw err; - }); - } + port(): number { + return (this.server.address() as Net.AddressInfo).port; + } - port(): number { - return (this.server.address() as Net.AddressInfo).port; + dispose() { + if (this.server) { + this.server.close(); } - - dispose() { - if (this.server) { - this.server.close(); - } - } + } } diff --git a/src/debugAdapter/debugAdapter.ts b/src/debugAdapter/debugAdapter.ts index 7dcd5b7..18f7b0f 100644 --- a/src/debugAdapter/debugAdapter.ts +++ b/src/debugAdapter/debugAdapter.ts @@ -13,78 +13,83 @@ import { FileAccessor, TEALDebuggingAssets } from './utils'; * Since here we run the debug adapter as a separate ("external") process, it has no access to VS Code API. * So we can only use node.js API for accessing files. */ -const fsAccessor: FileAccessor = { - isWindows: process.platform === 'win32', - readFile(path: string): Promise { - return fs.readFile(path); - }, - writeFile(path: string, contents: Uint8Array): Promise { - return fs.writeFile(path, contents); - } +const fsAccessor: FileAccessor = { + isWindows: process.platform === 'win32', + readFile(path: string): Promise { + return fs.readFile(path); + }, + writeFile(path: string, contents: Uint8Array): Promise { + return fs.writeFile(path, contents); + }, }; async function run() { - /* - * When the debug adapter is run as an external process, - * normally the helper function DebugSession.run(...) takes care of everything: - * - * MockDebugSession.run(MockDebugSession); - * - * but here the helper is not flexible enough to deal with a debug session constructors with a parameter. - * So for now we copied and modified the helper: - */ + /* + * When the debug adapter is run as an external process, + * normally the helper function DebugSession.run(...) takes care of everything: + * + * MockDebugSession.run(MockDebugSession); + * + * but here the helper is not flexible enough to deal with a debug session constructors with a parameter. + * So for now we copied and modified the helper: + */ - // first parse command line arguments to see whether the debug adapter should run as a server - let port = 0; - let simulateResponsePath = process.env.ALGORAND_SIMULATION_RESPONSE_PATH; - let txnGroupSourcesDescriptionPath = process.env.ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH; + // first parse command line arguments to see whether the debug adapter should run as a server + let port = 0; + const simulateResponsePath = process.env.ALGORAND_SIMULATION_RESPONSE_PATH; + const txnGroupSourcesDescriptionPath = + process.env.ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH; - const args = process.argv.slice(2); - args.forEach(function (val, index, array) { - const portMatch = /^--server=(\d{4,5})$/.exec(val); - if (portMatch) { - port = parseInt(portMatch[1], 10); - } - }); + const args = process.argv.slice(2); + args.forEach(function (val, index, array) { + const portMatch = /^--server=(\d{4,5})$/.exec(val); + if (portMatch) { + port = parseInt(portMatch[1], 10); + } + }); - if (typeof simulateResponsePath === 'undefined') { - throw new Error('missing ALGORAND_SIMULATION_RESPONSE_PATH'); - } - if (typeof txnGroupSourcesDescriptionPath === 'undefined') { - throw new Error('missing ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH'); - } + if (typeof simulateResponsePath === 'undefined') { + throw new Error('missing ALGORAND_SIMULATION_RESPONSE_PATH'); + } + if (typeof txnGroupSourcesDescriptionPath === 'undefined') { + throw new Error('missing ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH'); + } - const assets = await TEALDebuggingAssets.loadFromFiles(fsAccessor, path.resolve(simulateResponsePath), path.resolve(txnGroupSourcesDescriptionPath)); + const assets = await TEALDebuggingAssets.loadFromFiles( + fsAccessor, + path.resolve(simulateResponsePath), + path.resolve(txnGroupSourcesDescriptionPath), + ); - if (port > 0) { - // start a server that creates a new session for every connection request - console.error(`waiting for debug protocol on port ${port}`); - const server = Net.createServer((socket) => { - console.log('>> accepted connection from client'); - socket.on('error', (err) => { - throw err; - }); - socket.on('end', () => { - console.error('>> client connection closed\n'); - }); - const session = new TxnGroupDebugSession(fsAccessor, assets); - session.setRunAsServer(true); - session.start(socket, socket); - }).listen(port); - server.on('error', (err) => { - throw err; - }); - } else { - // start a single session that communicates via stdin/stdout - const session = new TxnGroupDebugSession(fsAccessor, assets); - process.on('SIGTERM', () => { - session.shutdown(); - }); - session.start(process.stdin, process.stdout); - } + if (port > 0) { + // start a server that creates a new session for every connection request + console.error(`waiting for debug protocol on port ${port}`); + const server = Net.createServer((socket) => { + console.log('>> accepted connection from client'); + socket.on('error', (err) => { + throw err; + }); + socket.on('end', () => { + console.error('>> client connection closed\n'); + }); + const session = new TxnGroupDebugSession(fsAccessor, assets); + session.setRunAsServer(true); + session.start(socket, socket); + }).listen(port); + server.on('error', (err) => { + throw err; + }); + } else { + // start a single session that communicates via stdin/stdout + const session = new TxnGroupDebugSession(fsAccessor, assets); + process.on('SIGTERM', () => { + session.shutdown(); + }); + session.start(process.stdin, process.stdout); + } } -run().catch(err => { - console.error(err); - process.exit(1); +run().catch((err) => { + console.error(err); + process.exit(1); }); diff --git a/src/debugAdapter/debugRequestHandlers.ts b/src/debugAdapter/debugRequestHandlers.ts index f186fb8..9c8f28f 100644 --- a/src/debugAdapter/debugRequestHandlers.ts +++ b/src/debugAdapter/debugRequestHandlers.ts @@ -1,24 +1,42 @@ import { - Logger, logger, - LoggingDebugSession, - InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, OutputEvent, - Thread, StackFrame, Scope, Source, Handles, Breakpoint + Logger, + logger, + LoggingDebugSession, + InitializedEvent, + TerminatedEvent, + StoppedEvent, + BreakpointEvent, + OutputEvent, + Thread, + StackFrame, + Scope, + Source, + Handles, + Breakpoint, } from '@vscode/debugadapter'; import { DebugProtocol } from '@vscode/debugprotocol'; import { basename } from 'path-browserify'; -import { TxnGroupWalkerRuntime, IRuntimeBreakpoint } from './txnGroupWalkerRuntime'; +import { + TxnGroupWalkerRuntime, + IRuntimeBreakpoint, +} from './txnGroupWalkerRuntime'; import { ProgramStackFrame } from './traceReplayEngine'; import { Subject } from 'await-notify'; import * as algosdk from 'algosdk'; -import { FileAccessor, TEALDebuggingAssets, isAsciiPrintable, limitArray } from './utils'; +import { + FileAccessor, + TEALDebuggingAssets, + isAsciiPrintable, + limitArray, +} from './utils'; export enum RuntimeEvents { - stopOnEntry = 'stopOnEntry', - stopOnStep = 'stopOnStep', - stopOnBreakpoint = 'stopOnBreakpoint', - breakpointValidated = 'breakpointValidated', - end = 'end', - error = 'error', + stopOnEntry = 'stopOnEntry', + stopOnStep = 'stopOnStep', + stopOnBreakpoint = 'stopOnBreakpoint', + breakpointValidated = 'breakpointValidated', + end = 'end', + error = 'error', } /** @@ -28,995 +46,1314 @@ export enum RuntimeEvents { * The interface should always match this schema. */ interface ILaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - /** An absolute path to the "program" to debug. */ - program: string; - /** Automatically stop target after launch. If not specified, target does not stop. */ - stopOnEntry?: boolean; - /** enable logging the Debug Adapter Protocol */ - trace?: boolean; - /** run without debugging */ - noDebug?: boolean; - /** if specified, results in a simulated compile error in launch. */ - compileError?: 'default' | 'show' | 'hide'; + /** An absolute path to the "program" to debug. */ + program: string; + /** Automatically stop target after launch. If not specified, target does not stop. */ + stopOnEntry?: boolean; + /** enable logging the Debug Adapter Protocol */ + trace?: boolean; + /** run without debugging */ + noDebug?: boolean; + /** if specified, results in a simulated compile error in launch. */ + compileError?: 'default' | 'show' | 'hide'; } -interface IAttachRequestArguments extends ILaunchRequestArguments { } - +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface IAttachRequestArguments extends ILaunchRequestArguments {} export class TxnGroupDebugSession extends LoggingDebugSession { - - // we don't support multiple threads, so we can use a hardcoded ID for the default thread - private static threadID = 1; - - // txn group walker runtime for walking txn group. - private _runtime: TxnGroupWalkerRuntime; - - private _variableHandles = new Handles(); - - private _sourceHandles = new Handles<{ content: string, mimeType?: string }>(); - - private _configurationDone = new Subject(); - - private _debugAssets: TEALDebuggingAssets; - - /** - * Creates a new debug adapter that is used for one debug session. - * We configure the default implementation of a debug adapter here. - */ - public constructor(fileAccessor: FileAccessor, debugAssets: TEALDebuggingAssets) { - super("mock-debug.txt"); - - this._debugAssets = debugAssets; - - // this debugger uses zero-based lines and columns - this.setDebuggerLinesStartAt1(false); - this.setDebuggerColumnsStartAt1(false); - - this._runtime = new TxnGroupWalkerRuntime(fileAccessor, this._debugAssets); - - // setup event handlers - this._runtime.on(RuntimeEvents.stopOnEntry, () => { - this.sendEvent(new StoppedEvent('entry', TxnGroupDebugSession.threadID)); - }); - this._runtime.on(RuntimeEvents.stopOnStep, () => { - this.sendEvent(new StoppedEvent('step', TxnGroupDebugSession.threadID)); - }); - this._runtime.on(RuntimeEvents.stopOnBreakpoint, () => { - this.sendEvent(new StoppedEvent('breakpoint', TxnGroupDebugSession.threadID)); - }); - this._runtime.on(RuntimeEvents.breakpointValidated, (bp: IRuntimeBreakpoint) => { - this.sendEvent(new BreakpointEvent('changed', { verified: bp.verified, column: bp.location.column, id: bp.id } as DebugProtocol.Breakpoint)); - }); - this._runtime.on(RuntimeEvents.end, () => { - this.sendEvent(new TerminatedEvent()); - }); - this._runtime.on('error', (err: Error) => { - this.sendEvent(new OutputEvent(err.message, 'stderr')); - }); - } - - /** - * The 'initialize' request is the first request called by the frontend - * to interrogate the features the debug adapter provides. - */ - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments) { - // build and return the capabilities of this debug adapter: - response.body = response.body || {}; - - // the adapter implements the configurationDone request. - response.body.supportsConfigurationDoneRequest = true; - - // make VS Code use 'evaluate' when hovering over source - response.body.supportsEvaluateForHovers = false; - - // make VS Code show a 'step back' button - response.body.supportsStepBack = true; - - // make VS Code send cancel request - response.body.supportsCancelRequest = false; - - // make VS Code send the breakpointLocations request - response.body.supportsBreakpointLocationsRequest = true; - - // make VS Code provide "Step in Target" functionality - response.body.supportsStepInTargetsRequest = true; - - // TEAL is not so thready. - response.body.supportsSingleThreadExecutionRequests = false; - response.body.supportsTerminateThreadsRequest = false; - - // the adapter defines two exceptions filters, one with support for conditions. - response.body.supportsExceptionFilterOptions = true; - response.body.exceptionBreakpointFilters = [ // TODO: make filter inner txn only - { - filter: 'namedException', - label: "Named Exception", - description: `Break on named exceptions. Enter the exception's name as the Condition.`, - default: false, - supportsCondition: true, - conditionDescription: `Enter the exception's name` - }, - { - filter: 'otherExceptions', - label: "Other Exceptions", - description: 'This is a other exception', - default: true, - supportsCondition: false - } - ]; - - // make VS Code send exceptionInfo request - // response.body.supportsExceptionInfoRequest = true; - - // make VS Code send setVariable request - // response.body.supportsSetVariable = true; - - // make VS Code send setExpression request - // response.body.supportsSetExpression = true; - - // make VS Code send disassemble request - // response.body.supportsDisassembleRequest = true; - // response.body.supportsSteppingGranularity = true; - // response.body.supportsInstructionBreakpoints = true; - - // make VS Code able to read and write variable memory - // response.body.supportsReadMemoryRequest = true; - // response.body.supportsWriteMemoryRequest = true; - - response.body.supportSuspendDebuggee = true; - response.body.supportTerminateDebuggee = true; - // response.body.supportsFunctionBreakpoints = true; - response.body.supportsDelayedStackTraceLoading = true; - - this.sendResponse(response); - - // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time, - // we request them early by sending an 'initializeRequest' to the frontend. - // The frontend will end the configuration sequence by calling 'configurationDone' request. - this.sendEvent(new InitializedEvent()); - } - - /** - * Called at the end of the configuration sequence. - * Indicates that all breakpoints etc. have been sent to the DA and that the 'launch' can start. - */ - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { - super.configurationDoneRequest(response, args); - - // notify the launchRequest that configuration has finished - this._configurationDone.notify(); - } - - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request): void { - console.log(`disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`); - } - - protected async attachRequest(response: DebugProtocol.AttachResponse, args: IAttachRequestArguments) { - return this.launchRequest(response, args); - } - - protected async launchRequest(response: DebugProtocol.LaunchResponse, args: ILaunchRequestArguments) { - - // make sure to 'Stop' the buffered logging if 'trace' is not set - logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, false); - - // TODO: use args.program - - // wait 1 second until configuration has finished (and configurationDoneRequest has been called) - await this._configurationDone.wait(1000); - - // start the program in the runtime - this._runtime.start(!!args.stopOnEntry, !args.noDebug); - - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - const { path } = args.source; - if (typeof path !== 'undefined') { - const clientBreakpoints = args.breakpoints || []; - - // clear all breakpoints for this file - this._runtime.clearBreakpoints(path); - - // set and verify breakpoint locations - const actualBreakpoints = clientBreakpoints.map(clientBp => { - const line = this.convertClientLineToDebugger(clientBp.line); - const column = typeof clientBp.column === 'number' ? this.convertClientColumnToDebugger(clientBp.column) : undefined; - const runtimeBreakpoint = this._runtime.setBreakPoint(path, line, column); - const bp = new Breakpoint( - runtimeBreakpoint.verified, - this.convertDebuggerLineToClient(runtimeBreakpoint.location.line), - typeof runtimeBreakpoint.location.column !== 'undefined' ? this.convertDebuggerColumnToClient(runtimeBreakpoint.location.column) : undefined - ) as DebugProtocol.Breakpoint; - bp.id = runtimeBreakpoint.id; - return bp; - }); - - // send back the actual breakpoint positions - response.body = { - breakpoints: actualBreakpoints - }; - } - - this.sendResponse(response); - } - - protected breakpointLocationsRequest(response: DebugProtocol.BreakpointLocationsResponse, args: DebugProtocol.BreakpointLocationsArguments, request?: DebugProtocol.Request): void { - const { path } = args.source; - if (typeof path !== 'undefined') { - const startLine = this.convertClientLineToDebugger(args.line); - const endLine = typeof args.endLine === 'number' ? this.convertClientLineToDebugger(args.endLine) : startLine; - const startColumn = typeof args.column === 'number' ? this.convertClientColumnToDebugger(args.column) : 0; - const endColumn = typeof args.endColumn === 'number' ? this.convertClientColumnToDebugger(args.endColumn) : Number.MAX_SAFE_INTEGER; - - const locations = this._runtime.breakpointLocations(path) - .filter(({ line, column }) => line >= startLine && line <= endLine && (typeof column !== 'undefined' ? column >= startColumn && column <= endColumn : true)); - - const responseBreakpoints: DebugProtocol.BreakpointLocation[] = []; - for (const location of locations) { - responseBreakpoints.push({ - line: this.convertDebuggerLineToClient(location.line), - column: typeof location.column !== 'undefined' ? this.convertDebuggerColumnToClient(location.column) : undefined, - }); - } - response.body = { - breakpoints: responseBreakpoints - }; - } - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - - // runtime supports no threads so just return a default thread. - response.body = { - threads: [ - new Thread(TxnGroupDebugSession.threadID, "thread 1"), - ] - }; - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0; - const maxLevels = typeof args.levels === 'number' ? args.levels : 1000; - - // The runtime has a stack where the latest call is the last element. We need to return the - // reverse of that. - const adjustedEndFrame = this._runtime.stackLength() - startFrame; - const adjustedStartFrame = Math.max(0, adjustedEndFrame - maxLevels); - - const stk = this._runtime.stack(adjustedStartFrame, adjustedEndFrame); - - const stackFramesForResponse = stk.frames.map((frame, index) => { - const id = adjustedStartFrame + index; - - const sourceFile = frame.sourceFile(); - let source: Source | undefined = undefined; - if (typeof sourceFile.path !== 'undefined') { - source = this.createSource(sourceFile.path); - } else if (typeof sourceFile.content !== 'undefined') { - source = this.createSourceWithContent(sourceFile.name, sourceFile.content, sourceFile.contentMimeType); - } - - const sourceLocation = frame.sourceLocation(); - const line = this.convertDebuggerLineToClient(sourceLocation.line); - const column = typeof sourceLocation.column !== 'undefined' ? this.convertDebuggerColumnToClient(sourceLocation.column) : undefined; - - const protocolFrame = new StackFrame(id, frame.name(), source, line, column); - protocolFrame.endLine = sourceLocation.endLine; - protocolFrame.endColumn = sourceLocation.endColumn; - return protocolFrame; - }); - stackFramesForResponse.reverse(); - - response.body = { - totalFrames: stk.count, - stackFrames: stackFramesForResponse, - // 4 options for 'totalFrames': - //omit totalFrames property: // VS Code has to probe/guess. Should result in a max. of two requests - // totalFrames: stk.count // stk.count is the correct size, should result in a max. of two requests - //totalFrames: 1000000 // not the correct size, should result in a max. of two requests - //totalFrames: endFrame + 20 // dynamically increases the size with every requested chunk, results in paging - }; - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { - const frame = this._runtime.getStackFrame(args.frameId); - - let scopes: DebugProtocol.Scope[] = []; - if (typeof frame !== 'undefined') { - if (frame instanceof ProgramStackFrame) { - const programScope = new ProgramStateScope(args.frameId); - let scopeName = 'Program State'; - const appID = frame.currentAppID(); - if (typeof appID !== 'undefined') { - scopeName += `: App ${appID}`; - } - scopes.push( - new Scope(scopeName, this._variableHandles.create(programScope), false) - ); - } - scopes.push( - new Scope("On-chain State", this._variableHandles.create('chain'), false) - ); - } - - response.body = { scopes }; - this.sendResponse(response); - } - - protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request): Promise { - let variables: DebugProtocol.Variable[] = []; - - const v = this._variableHandles.get(args.variablesReference); - - if (v instanceof ProgramStateScope) { - const frame = this._runtime.getStackFrame(v.frameIndex); - if (!frame || !(frame instanceof ProgramStackFrame)) { - throw new Error(`Unexpected frame: ${typeof frame}`); - } - const programState = frame.state; - - if (typeof v.specificState === 'undefined') { - variables = [ - { - name: 'pc', - value: programState.pc.toString(), - type: 'uint64', - variablesReference: 0, - evaluateName: 'pc' - }, - { - name: 'stack', - value: programState.stack.length === 0 ? '[]' : '[...]', - type: 'array', - variablesReference: this._variableHandles.create(new ProgramStateScope(v.frameIndex, 'stack')), - indexedVariables: programState.stack.length, - presentationHint: { - kind: 'data', - }, - }, - { - name: 'scratch', - value: '[...]', - type: 'array', - variablesReference: this._variableHandles.create(new ProgramStateScope(v.frameIndex, 'scratch')), - indexedVariables: 256, - presentationHint: { - kind: 'data', - }, - } - ]; - } else if (v.specificState === 'stack') { - if (args.filter !== 'named') { - variables = programState.stack.map((value, index) => this.convertAvmValue(v, value, index)); - } - } else if (v.specificState === 'scratch') { - const expandedScratch: algosdk.modelsv2.AvmValue[] = []; - for (let i = 0; i < 256; i++) { - expandedScratch.push(programState.scratch.get(i) || new algosdk.modelsv2.AvmValue({ type: 2 })); - } - if (args.filter !== 'named') { - variables = expandedScratch.map((value, index) => this.convertAvmValue(v, value, index)); - } - } - } else if (v === 'chain') { - const appIDs = this._runtime.getAppStateReferences(); - variables = [{ - name: 'app', - value: '', - type: 'object', - variablesReference: this._variableHandles.create('app'), - namedVariables: appIDs.length - }]; - } else if (v === 'app') { - const appIDs = this._runtime.getAppStateReferences(); - variables = appIDs.map(appID => ({ - name: appID.toString(), - value: '', - type: 'object', - variablesReference: this._variableHandles.create(new AppStateScope(appID)), - namedVariables: 3, - })); - } else if (v instanceof AppStateScope) { - variables = [ - { - name: 'global', - value: '', - type: 'object', - variablesReference: this._variableHandles.create(new AppSpecificStateScope({ scope: 'global', appID: v.appID })), - namedVariables: 1, // TODO - }, - { - name: 'local', - value: '', - type: 'object', - variablesReference: this._variableHandles.create(new AppSpecificStateScope({ scope: 'local', appID: v.appID })), - namedVariables: 1, // TODO - }, - { - name: 'box', - value: '', - type: 'object', - variablesReference: this._variableHandles.create(new AppSpecificStateScope({ scope: 'box', appID: v.appID })), - namedVariables: 1, // TODO - } - ]; - } else if (v instanceof AppSpecificStateScope) { - const state = this._runtime.getAppState(v.appID); - if (v.scope === 'global') { - variables = state.globalStateArray().map(kv => this.convertAvmKeyValue(v, kv)); - } else if (v.scope === 'local') { - if (typeof v.account === 'undefined') { - const accounts = this._runtime.getAppLocalStateAccounts(v.appID); - variables = accounts.map(account => ({ - name: account, - value: 'local state', - type: 'object', - variablesReference: this._variableHandles.create(new AppSpecificStateScope({ scope: 'local', appID: v.appID, account })), - namedVariables: 1, // TODO - evaluateName: evaluateNameForScope(v, account), - })); - } else { - variables = state.localStateArray(v.account).map(kv => this.convertAvmKeyValue(v, kv)); - } - } else if (v.scope === 'box') { - variables = state.boxStateArray().map(kv => this.convertAvmKeyValue(v, kv)); - } - } else if (v instanceof AvmValueReference) { - if (v.scope instanceof ProgramStateScope) { - const frame = this._runtime.getStackFrame(v.scope.frameIndex); - if (!frame || !(frame instanceof ProgramStackFrame)) { - throw new Error(`Unexpected frame: ${typeof frame}`); - } - let toExpand: algosdk.modelsv2.AvmValue; - if (v.scope.specificState === 'stack') { - toExpand = frame.state.stack[v.key as number]; - } else if (v.scope.specificState === 'scratch') { - toExpand = frame.state.scratch.get(v.key as number) || new algosdk.modelsv2.AvmValue({ type: 2 }); - } else { - throw new Error(`Unexpected AvmValueReference scope: ${v.scope}`); - } - variables = this.expandAvmValue(toExpand, args.filter); - } else if (v.scope instanceof AppSpecificStateScope && typeof v.key === 'string' && v.key.startsWith('0x')) { - let toExpand: algosdk.modelsv2.AvmKeyValue; - const state = this._runtime.getAppState(v.scope.appID); - const keyHex = v.key.slice(2); - if (v.scope.scope === 'global') { - const value = state.globalState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - toExpand = new algosdk.modelsv2.AvmKeyValue({ key: keyBytes, value }); - } else { - throw new Error(`key "${v.key}" not found in global state`); - } - } else if (v.scope.scope === 'local') { - if (typeof v.scope.account === 'undefined') { - throw new Error('this shouldn\'t happen: ' + JSON.stringify(v)); - } else { - const accountState = state.localState.get(v.scope.account); - if (!accountState) { - throw new Error(`account "${v.scope.account}" not found in local state`); - } - const value = accountState.getHex(keyHex); - if (!value) { - throw new Error(`key "${v.key}" not found in local state for account "${v.scope.account}"`); - } - toExpand = new algosdk.modelsv2.AvmKeyValue({ key: Buffer.from(keyHex, 'hex'), value }); - } - } else if (v.scope.scope === 'box') { - const value = state.boxState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - toExpand = new algosdk.modelsv2.AvmKeyValue({ key: keyBytes, value }); - } else { - throw new Error(`key "${v.key}" not found in box state`); - } - } else { - throw new Error(`Unexpected AppSpecificStateScope scope: ${v.scope}`); - } - variables = this.expandAvmKeyValue(v.scope, toExpand, args.filter); - } - } - - variables = limitArray(variables, args.start, args.count); - - response.body = { - variables - }; - this.sendResponse(response); - } - - protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): Promise { - let reply: string | undefined; - let rv: DebugProtocol.Variable | undefined = undefined; - - // Note, can use args.context to perform different actions based on where the expression is evaluated - - let result: [AvmValueScope, number | string] | undefined = undefined; - try { - result = evaluateNameToScope(args.expression); - } catch (e) { - reply = (e as Error).message; - } - - if (result) { - const [scope, key] = result; - if (scope instanceof ProgramStateScope) { - if (typeof args.frameId === 'undefined') { - reply = 'frameId required for program state'; - } else { - const scopeWithFrame = new ProgramStateScope(args.frameId, scope.specificState); - const frame = this._runtime.getStackFrame(args.frameId); - if (!frame || !(frame instanceof ProgramStackFrame)) { - reply = `Unexpected frame: ${typeof frame}`; - } else { - if (scope.specificState === 'pc') { - rv = { - name: 'pc', - value: frame.state.pc.toString(), - type: 'uint64', - variablesReference: 0, - evaluateName: 'pc' - }; - } else if (scope.specificState === 'stack') { - let index = key as number; - const stackValues = frame.state.stack; - if (index < 0) { - const adjustedIndex = index + stackValues.length; - if (adjustedIndex < 0) { - reply = `stack[${index}] out of range`; - } else { - index = adjustedIndex; - } - } - if (0 <= index && index < stackValues.length) { - rv = this.convertAvmValue(scopeWithFrame, stackValues[index], index); - } else if (index < 0 && stackValues.length + index >= 0) { - rv = this.convertAvmValue(scopeWithFrame, stackValues[stackValues.length + index], index); - } else { - reply = `stack[${index}] out of range`; - } - } else if (scope.specificState === 'scratch') { - const index = key as number; - if (0 <= index && index < 256) { - rv = this.convertAvmValue(scopeWithFrame, frame.state.scratch.get(index) || new algosdk.modelsv2.AvmValue({ type: 2 }), index); - } else { - reply = `scratch[${index}] out of range`; - } - } - } - } - } else if (typeof key === 'string') { - const state = this._runtime.getAppState(scope.appID); - if (scope.property) { - reply = `cannot evaluate property "${scope.property}"`; - } else if (scope.scope === 'global' && key.startsWith('0x')) { - const keyHex = key.slice(2); - const value = state.globalState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - const kv = new algosdk.modelsv2.AvmKeyValue({ key: keyBytes, value }); - rv = this.convertAvmKeyValue(scope, kv); - } else { - reply = `key "${key}" not found in global state`; - } - } else if (scope.scope === 'local') { - if (typeof scope.account === 'undefined') { - rv = { - name: key, - value: 'local state', - type: 'object', - variablesReference: this._variableHandles.create(new AppSpecificStateScope({ scope: 'local', appID: scope.appID, account: key })), - namedVariables: 1, // TODO - evaluateName: evaluateNameForScope(scope, key), - }; - } else { - const accountState = state.localState.get(scope.account); - if (!accountState) { - reply = `account "${scope.account}" not found in local state`; - } else if (key.startsWith('0x')) { - const keyHex = key.slice(2); - const value = accountState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - const kv = new algosdk.modelsv2.AvmKeyValue({ key: keyBytes, value }); - rv = this.convertAvmKeyValue(scope, kv); - } else { - reply = `key "${key}" not found in local state for account "${scope.account}"`; - } - } else { - reply = `cannot evaluate property "${key}"`; - } - } - } else if (scope.scope === 'box' && key.startsWith('0x')) { - const keyHex = key.slice(2); - const value = state.boxState.getHex(keyHex); - if (value) { - const keyBytes = Buffer.from(keyHex, 'hex'); - const kv = new algosdk.modelsv2.AvmKeyValue({ key: keyBytes, value }); - rv = this.convertAvmKeyValue(scope, kv); - } else { - reply = `key "${key}" not found in box state`; - } - } - } - } - - if (rv) { - response.body = { - result: rv.value, - type: rv.type, - variablesReference: rv.variablesReference, - presentationHint: rv.presentationHint - }; - } else { - response.body = { - result: reply || `unknown expression: "${args.expression}"`, - variablesReference: 0 - }; - } - - this.sendResponse(response); - } - - protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments, request?: DebugProtocol.Request): void { - const sourceInfo = this._sourceHandles.get(args.sourceReference); - if (typeof sourceInfo !== 'undefined') { - response.body = { - content: sourceInfo.content, - mimeType: sourceInfo.mimeType - }; - } else { - response.body = { - content: `source not available` - }; - } - this.sendResponse(response); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.executionResumed(); - this._runtime.continue(false); - this.sendResponse(response); - } - - protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void { - this.executionResumed(); - this._runtime.continue(true); - this.sendResponse(response); - } - - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.executionResumed(); - this._runtime.step(false); - this.sendResponse(response); - } - - protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void { - this.executionResumed(); - this._runtime.step(true); - this.sendResponse(response); - } - - protected stepInTargetsRequest(response: DebugProtocol.StepInTargetsResponse, args: DebugProtocol.StepInTargetsArguments) { - const targets = this._runtime.getStepInTargets(args.frameId); - response.body = { - targets: targets.map(t => { - return { id: t.id, label: t.label }; - }) - }; - this.sendResponse(response); - } - - protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void { - this.executionResumed(); - this._runtime.stepIn(args.targetId); - this.sendResponse(response); - } - - protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void { - this.executionResumed(); - this._runtime.stepOut(); - this.sendResponse(response); - } - - private executionResumed(): void { - this._variableHandles.reset(); - this._sourceHandles.reset(); - } - - //---- helpers - - private convertAvmValue(scope: AvmValueScope, avmValue: algosdk.modelsv2.AvmValue, key: number | string, overrideVariableReference?: boolean): DebugProtocol.Variable { - let namedVariables: number | undefined = undefined; - let indexedVariables: number | undefined = undefined; - let presentationHint: DebugProtocol.VariablePresentationHint | undefined = undefined; - let makeVariableReference = false; - if (avmValue.type === 1) { - // byte array - const bytes = avmValue.bytes || new Uint8Array(); - namedVariables = 2; - if (isAsciiPrintable(bytes)) { - namedVariables++; - } - indexedVariables = bytes.length; - presentationHint = { - kind: 'data', - attributes: ['rawString'], - }; - makeVariableReference = true; - } - if (typeof overrideVariableReference !== 'undefined') { - makeVariableReference = overrideVariableReference; - } - return { - name: key.toString(), - value: this.avmValueToString(avmValue), - type: avmValue.type === 1 ? 'byte[]' : 'uint64', - variablesReference: makeVariableReference ? this._variableHandles.create(new AvmValueReference(scope, key)) : 0, - namedVariables, - indexedVariables, - presentationHint, - evaluateName: evaluateNameForScope(scope, key), - }; - } - - private expandAvmValue(avmValue: algosdk.modelsv2.AvmValue, filter?: DebugProtocol.VariablesArguments['filter']): DebugProtocol.Variable[] { - if (avmValue.type !== 1) { - return []; - } - - const bytes = avmValue.bytes || new Uint8Array(); - - const values: DebugProtocol.Variable[] = []; - - if (filter !== 'indexed') { - let formats: BufferEncoding[] = ['hex', 'base64']; - if (isAsciiPrintable(bytes)) { - // TODO: perhaps do this with UTF-8 instead, see https://stackoverflow.com/questions/75108373/how-to-check-if-a-node-js-buffer-contains-valid-utf-8 - formats.push('ascii'); - } - if (bytes.length === 0) { - formats = []; - } - - for (const format of formats) { - values.push({ - name: format, - type: 'string', - value: Buffer.from(bytes).toString(format), - variablesReference: 0, - }); - } - - if (bytes.length === 32) { - values.push({ - name: 'address', - type: 'string', - value: algosdk.encodeAddress(bytes), - variablesReference: 0, - }); - } - - values.push({ - name: 'length', - type: 'int', - value: bytes.length.toString(), - variablesReference: 0, - }); - } - - if (filter !== 'named') { - for (let i = 0; i < bytes.length; i++) { - values.push({ - name: i.toString(), - type: 'uint8', - value: bytes[i].toString(), - variablesReference: 0, - }); - } - } - - return values; - } - - private convertAvmKeyValue(scope: AvmValueScope, avmKeyValue: algosdk.modelsv2.AvmKeyValue): DebugProtocol.Variable { - const keyString = '0x' + Buffer.from(avmKeyValue.key || new Uint8Array()).toString('hex'); - const value = this.convertAvmValue(scope, avmKeyValue.value, keyString, true); - delete value.indexedVariables; - value.namedVariables = 2; - return value; - } - - private expandAvmKeyValue(scope: AppSpecificStateScope, avmKeyValue: algosdk.modelsv2.AvmKeyValue, filter?: DebugProtocol.VariablesArguments['filter']): DebugProtocol.Variable[] { - if (typeof scope.property === 'undefined') { - if (filter === 'indexed') { - return []; - } - const keyString = '0x' + Buffer.from(avmKeyValue.key || new Uint8Array()).toString('hex'); - const keyScope = new AppSpecificStateScope({ scope: scope.scope, appID: scope.appID, account: scope.account, property: 'key' }); - const valueScope = new AppSpecificStateScope({ scope: scope.scope, appID: scope.appID, account: scope.account, property: 'value' }); - const keyVariable = this.convertAvmValue(keyScope, new algosdk.modelsv2.AvmValue({ type: 1, bytes: avmKeyValue.key }), '', false); - const valueVariable = this.convertAvmValue(valueScope, avmKeyValue.value, '', false); - const valueHasChildren = valueVariable.namedVariables || valueVariable.indexedVariables; - return [ - { - name: 'key', - type: keyVariable.type, - value: keyVariable.value, - variablesReference: this._variableHandles.create(new AvmValueReference(keyScope, keyString)), - namedVariables: keyVariable.namedVariables, - indexedVariables: keyVariable.indexedVariables, - presentationHint: keyVariable.presentationHint, - // evaluateName: evaluateNameForScope(keyScope, keyString), - }, { - name: 'value', - type: keyVariable.type, - value: valueVariable.value, - variablesReference: valueHasChildren ? this._variableHandles.create(new AvmValueReference(valueScope, keyString)) : 0, - namedVariables: keyVariable.namedVariables, - indexedVariables: keyVariable.indexedVariables, - presentationHint: keyVariable.presentationHint, - // evaluateName: valueHasChildren ? evaluateNameForScope(valueScope, keyString) : '', - } - ]; - } - - if (scope.property === 'key') { - const avmKey = new algosdk.modelsv2.AvmValue({ type: 1, bytes: avmKeyValue.key }); - return this.expandAvmValue(avmKey, filter); - } - - return this.expandAvmValue(avmKeyValue.value, filter); - } - - private avmValueToString(avmValue: algosdk.modelsv2.AvmValue): string { - if (avmValue.type === 1) { - // byte array - const bytes = avmValue.bytes || new Uint8Array(); - return '0x' + Buffer.from(bytes).toString('hex'); - } - // uint64 - const uint = avmValue.uint || 0; - return uint.toString(); - } - - private createSource(filePath: string): Source { - return new Source(basename(filePath), this.convertDebuggerPathToClient(filePath), undefined, undefined, 'teal-txn-group-adapter-data'); - } - - private createSourceWithContent(fileName: string, content: string, mimeType?: string) { - const id = this._sourceHandles.create({ content, mimeType }); - return new Source(fileName, undefined, id); - } + // we don't support multiple threads, so we can use a hardcoded ID for the default thread + private static threadID = 1; + + // txn group walker runtime for walking txn group. + private _runtime: TxnGroupWalkerRuntime; + + private _variableHandles = new Handles< + | ProgramStateScope + | OnChainStateScope + | AppStateScope + | AppSpecificStateScope + | AvmValueReference + >(); + + private _sourceHandles = new Handles<{ + content: string; + mimeType?: string; + }>(); + + private _configurationDone = new Subject(); + + private _debugAssets: TEALDebuggingAssets; + + /** + * Creates a new debug adapter that is used for one debug session. + * We configure the default implementation of a debug adapter here. + */ + public constructor( + fileAccessor: FileAccessor, + debugAssets: TEALDebuggingAssets, + ) { + super('mock-debug.txt'); + + this._debugAssets = debugAssets; + + // this debugger uses zero-based lines and columns + this.setDebuggerLinesStartAt1(false); + this.setDebuggerColumnsStartAt1(false); + + this._runtime = new TxnGroupWalkerRuntime(fileAccessor, this._debugAssets); + + // setup event handlers + this._runtime.on(RuntimeEvents.stopOnEntry, () => { + this.sendEvent(new StoppedEvent('entry', TxnGroupDebugSession.threadID)); + }); + this._runtime.on(RuntimeEvents.stopOnStep, () => { + this.sendEvent(new StoppedEvent('step', TxnGroupDebugSession.threadID)); + }); + this._runtime.on(RuntimeEvents.stopOnBreakpoint, () => { + this.sendEvent( + new StoppedEvent('breakpoint', TxnGroupDebugSession.threadID), + ); + }); + this._runtime.on( + RuntimeEvents.breakpointValidated, + (bp: IRuntimeBreakpoint) => { + this.sendEvent( + new BreakpointEvent('changed', { + verified: bp.verified, + column: bp.location.column, + id: bp.id, + } as DebugProtocol.Breakpoint), + ); + }, + ); + this._runtime.on(RuntimeEvents.end, () => { + this.sendEvent(new TerminatedEvent()); + }); + this._runtime.on('error', (err: Error) => { + this.sendEvent(new OutputEvent(err.message, 'stderr')); + }); + } + + /** + * The 'initialize' request is the first request called by the frontend + * to interrogate the features the debug adapter provides. + */ + protected initializeRequest( + response: DebugProtocol.InitializeResponse, + args: DebugProtocol.InitializeRequestArguments, + ) { + // build and return the capabilities of this debug adapter: + response.body = response.body || {}; + + // the adapter implements the configurationDone request. + response.body.supportsConfigurationDoneRequest = true; + + // make VS Code use 'evaluate' when hovering over source + response.body.supportsEvaluateForHovers = false; + + // make VS Code show a 'step back' button + response.body.supportsStepBack = true; + + // make VS Code send cancel request + response.body.supportsCancelRequest = false; + + // make VS Code send the breakpointLocations request + response.body.supportsBreakpointLocationsRequest = true; + + // make VS Code provide "Step in Target" functionality + response.body.supportsStepInTargetsRequest = true; + + // TEAL is not so thready. + response.body.supportsSingleThreadExecutionRequests = false; + response.body.supportsTerminateThreadsRequest = false; + + // the adapter defines two exceptions filters, one with support for conditions. + response.body.supportsExceptionFilterOptions = true; + response.body.exceptionBreakpointFilters = [ + // TODO: make filter inner txn only + { + filter: 'namedException', + label: 'Named Exception', + description: `Break on named exceptions. Enter the exception's name as the Condition.`, + default: false, + supportsCondition: true, + conditionDescription: `Enter the exception's name`, + }, + { + filter: 'otherExceptions', + label: 'Other Exceptions', + description: 'This is a other exception', + default: true, + supportsCondition: false, + }, + ]; + + // make VS Code send exceptionInfo request + // response.body.supportsExceptionInfoRequest = true; + + // make VS Code send setVariable request + // response.body.supportsSetVariable = true; + + // make VS Code send setExpression request + // response.body.supportsSetExpression = true; + + // make VS Code send disassemble request + // response.body.supportsDisassembleRequest = true; + // response.body.supportsSteppingGranularity = true; + // response.body.supportsInstructionBreakpoints = true; + + // make VS Code able to read and write variable memory + // response.body.supportsReadMemoryRequest = true; + // response.body.supportsWriteMemoryRequest = true; + + response.body.supportSuspendDebuggee = true; + response.body.supportTerminateDebuggee = true; + // response.body.supportsFunctionBreakpoints = true; + response.body.supportsDelayedStackTraceLoading = true; + + this.sendResponse(response); + + // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time, + // we request them early by sending an 'initializeRequest' to the frontend. + // The frontend will end the configuration sequence by calling 'configurationDone' request. + this.sendEvent(new InitializedEvent()); + } + + /** + * Called at the end of the configuration sequence. + * Indicates that all breakpoints etc. have been sent to the DA and that the 'launch' can start. + */ + protected configurationDoneRequest( + response: DebugProtocol.ConfigurationDoneResponse, + args: DebugProtocol.ConfigurationDoneArguments, + ): void { + super.configurationDoneRequest(response, args); + + // notify the launchRequest that configuration has finished + this._configurationDone.notify(); + } + + protected disconnectRequest( + response: DebugProtocol.DisconnectResponse, + args: DebugProtocol.DisconnectArguments, + request?: DebugProtocol.Request, + ): void { + console.log( + `disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`, + ); + } + + protected async attachRequest( + response: DebugProtocol.AttachResponse, + args: IAttachRequestArguments, + ) { + return this.launchRequest(response, args); + } + + protected async launchRequest( + response: DebugProtocol.LaunchResponse, + args: ILaunchRequestArguments, + ) { + // make sure to 'Stop' the buffered logging if 'trace' is not set + logger.setup( + args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, + false, + ); + + // TODO: use args.program + + // wait 1 second until configuration has finished (and configurationDoneRequest has been called) + await this._configurationDone.wait(1000); + + // start the program in the runtime + this._runtime.start(!!args.stopOnEntry, !args.noDebug); + + this.sendResponse(response); + } + + protected setBreakPointsRequest( + response: DebugProtocol.SetBreakpointsResponse, + args: DebugProtocol.SetBreakpointsArguments, + ): void { + const { path } = args.source; + if (typeof path !== 'undefined') { + const clientBreakpoints = args.breakpoints || []; + + // clear all breakpoints for this file + this._runtime.clearBreakpoints(path); + + // set and verify breakpoint locations + const actualBreakpoints = clientBreakpoints.map((clientBp) => { + const line = this.convertClientLineToDebugger(clientBp.line); + const column = + typeof clientBp.column === 'number' + ? this.convertClientColumnToDebugger(clientBp.column) + : undefined; + const runtimeBreakpoint = this._runtime.setBreakPoint( + path, + line, + column, + ); + const bp = new Breakpoint( + runtimeBreakpoint.verified, + this.convertDebuggerLineToClient(runtimeBreakpoint.location.line), + typeof runtimeBreakpoint.location.column !== 'undefined' + ? this.convertDebuggerColumnToClient( + runtimeBreakpoint.location.column, + ) + : undefined, + ) as DebugProtocol.Breakpoint; + bp.id = runtimeBreakpoint.id; + return bp; + }); + + // send back the actual breakpoint positions + response.body = { + breakpoints: actualBreakpoints, + }; + } + + this.sendResponse(response); + } + + protected breakpointLocationsRequest( + response: DebugProtocol.BreakpointLocationsResponse, + args: DebugProtocol.BreakpointLocationsArguments, + request?: DebugProtocol.Request, + ): void { + const { path } = args.source; + if (typeof path !== 'undefined') { + const startLine = this.convertClientLineToDebugger(args.line); + const endLine = + typeof args.endLine === 'number' + ? this.convertClientLineToDebugger(args.endLine) + : startLine; + const startColumn = + typeof args.column === 'number' + ? this.convertClientColumnToDebugger(args.column) + : 0; + const endColumn = + typeof args.endColumn === 'number' + ? this.convertClientColumnToDebugger(args.endColumn) + : Number.MAX_SAFE_INTEGER; + + const locations = this._runtime + .breakpointLocations(path) + .filter( + ({ line, column }) => + line >= startLine && + line <= endLine && + (typeof column !== 'undefined' + ? column >= startColumn && column <= endColumn + : true), + ); + + const responseBreakpoints: DebugProtocol.BreakpointLocation[] = []; + for (const location of locations) { + responseBreakpoints.push({ + line: this.convertDebuggerLineToClient(location.line), + column: + typeof location.column !== 'undefined' + ? this.convertDebuggerColumnToClient(location.column) + : undefined, + }); + } + response.body = { + breakpoints: responseBreakpoints, + }; + } + this.sendResponse(response); + } + + protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { + // runtime supports no threads so just return a default thread. + response.body = { + threads: [new Thread(TxnGroupDebugSession.threadID, 'thread 1')], + }; + this.sendResponse(response); + } + + protected stackTraceRequest( + response: DebugProtocol.StackTraceResponse, + args: DebugProtocol.StackTraceArguments, + ): void { + const startFrame = + typeof args.startFrame === 'number' ? args.startFrame : 0; + const maxLevels = typeof args.levels === 'number' ? args.levels : 1000; + + // The runtime has a stack where the latest call is the last element. We need to return the + // reverse of that. + const adjustedEndFrame = this._runtime.stackLength() - startFrame; + const adjustedStartFrame = Math.max(0, adjustedEndFrame - maxLevels); + + const stk = this._runtime.stack(adjustedStartFrame, adjustedEndFrame); + + const stackFramesForResponse = stk.frames.map((frame, index) => { + const id = adjustedStartFrame + index; + + const sourceFile = frame.sourceFile(); + let source: Source | undefined = undefined; + if (typeof sourceFile.path !== 'undefined') { + source = this.createSource(sourceFile.path); + } else if (typeof sourceFile.content !== 'undefined') { + source = this.createSourceWithContent( + sourceFile.name, + sourceFile.content, + sourceFile.contentMimeType, + ); + } + + const sourceLocation = frame.sourceLocation(); + const line = this.convertDebuggerLineToClient(sourceLocation.line); + const column = + typeof sourceLocation.column !== 'undefined' + ? this.convertDebuggerColumnToClient(sourceLocation.column) + : undefined; + + const protocolFrame = new StackFrame( + id, + frame.name(), + source, + line, + column, + ); + protocolFrame.endLine = + typeof sourceLocation.endLine !== 'undefined' + ? this.convertDebuggerLineToClient(sourceLocation.endLine) + : undefined; + protocolFrame.endColumn = + typeof sourceLocation.endColumn !== 'undefined' + ? this.convertDebuggerColumnToClient(sourceLocation.endColumn) + : undefined; + return protocolFrame; + }); + stackFramesForResponse.reverse(); + + response.body = { + totalFrames: stk.count, + stackFrames: stackFramesForResponse, + // 4 options for 'totalFrames': + //omit totalFrames property: // VS Code has to probe/guess. Should result in a max. of two requests + // totalFrames: stk.count // stk.count is the correct size, should result in a max. of two requests + //totalFrames: 1000000 // not the correct size, should result in a max. of two requests + //totalFrames: endFrame + 20 // dynamically increases the size with every requested chunk, results in paging + }; + this.sendResponse(response); + } + + protected scopesRequest( + response: DebugProtocol.ScopesResponse, + args: DebugProtocol.ScopesArguments, + ): void { + const frame = this._runtime.getStackFrame(args.frameId); + + const scopes: DebugProtocol.Scope[] = []; + if (typeof frame !== 'undefined') { + if (frame instanceof ProgramStackFrame) { + const programScope = new ProgramStateScope(args.frameId); + let scopeName = 'Program State'; + const appID = frame.currentAppID(); + if (typeof appID !== 'undefined') { + scopeName += `: App ${appID}`; + } + scopes.push( + new Scope( + scopeName, + this._variableHandles.create(programScope), + false, + ), + ); + } + scopes.push( + new Scope( + 'On-chain State', + this._variableHandles.create('chain'), + false, + ), + ); + } + + response.body = { scopes }; + this.sendResponse(response); + } + + protected async variablesRequest( + response: DebugProtocol.VariablesResponse, + args: DebugProtocol.VariablesArguments, + request?: DebugProtocol.Request, + ): Promise { + let variables: DebugProtocol.Variable[] = []; + + const v = this._variableHandles.get(args.variablesReference); + + if (v instanceof ProgramStateScope) { + const frame = this._runtime.getStackFrame(v.frameIndex); + if (!frame || !(frame instanceof ProgramStackFrame)) { + throw new Error(`Unexpected frame: ${typeof frame}`); + } + const programState = frame.state; + + if (typeof v.specificState === 'undefined') { + variables = [ + { + name: 'pc', + value: programState.pc.toString(), + type: 'uint64', + variablesReference: 0, + evaluateName: 'pc', + }, + { + name: 'stack', + value: programState.stack.length === 0 ? '[]' : '[...]', + type: 'array', + variablesReference: this._variableHandles.create( + new ProgramStateScope(v.frameIndex, 'stack'), + ), + indexedVariables: programState.stack.length, + presentationHint: { + kind: 'data', + }, + }, + { + name: 'scratch', + value: '[...]', + type: 'array', + variablesReference: this._variableHandles.create( + new ProgramStateScope(v.frameIndex, 'scratch'), + ), + indexedVariables: 256, + presentationHint: { + kind: 'data', + }, + }, + ]; + } else if (v.specificState === 'stack') { + if (args.filter !== 'named') { + variables = programState.stack.map((value, index) => + this.convertAvmValue(v, value, index), + ); + } + } else if (v.specificState === 'scratch') { + const expandedScratch: algosdk.modelsv2.AvmValue[] = []; + for (let i = 0; i < 256; i++) { + expandedScratch.push( + programState.scratch.get(i) || + new algosdk.modelsv2.AvmValue({ type: 2 }), + ); + } + if (args.filter !== 'named') { + variables = expandedScratch.map((value, index) => + this.convertAvmValue(v, value, index), + ); + } + } + } else if (v === 'chain') { + const appIDs = this._runtime.getAppStateReferences(); + variables = [ + { + name: 'app', + value: '', + type: 'object', + variablesReference: this._variableHandles.create('app'), + namedVariables: appIDs.length, + }, + ]; + } else if (v === 'app') { + const appIDs = this._runtime.getAppStateReferences(); + variables = appIDs.map((appID) => ({ + name: appID.toString(), + value: '', + type: 'object', + variablesReference: this._variableHandles.create( + new AppStateScope(appID), + ), + namedVariables: 3, + })); + } else if (v instanceof AppStateScope) { + variables = [ + { + name: 'global', + value: '', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ scope: 'global', appID: v.appID }), + ), + namedVariables: 1, // TODO + }, + { + name: 'local', + value: '', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ scope: 'local', appID: v.appID }), + ), + namedVariables: 1, // TODO + }, + { + name: 'box', + value: '', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ scope: 'box', appID: v.appID }), + ), + namedVariables: 1, // TODO + }, + ]; + } else if (v instanceof AppSpecificStateScope) { + const state = this._runtime.getAppState(v.appID); + if (v.scope === 'global') { + variables = state + .globalStateArray() + .map((kv) => this.convertAvmKeyValue(v, kv)); + } else if (v.scope === 'local') { + if (typeof v.account === 'undefined') { + const accounts = this._runtime.getAppLocalStateAccounts(v.appID); + variables = accounts.map((account) => ({ + name: account, + value: 'local state', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ + scope: 'local', + appID: v.appID, + account, + }), + ), + namedVariables: 1, // TODO + evaluateName: evaluateNameForScope(v, account), + })); + } else { + variables = state + .localStateArray(v.account) + .map((kv) => this.convertAvmKeyValue(v, kv)); + } + } else if (v.scope === 'box') { + variables = state + .boxStateArray() + .map((kv) => this.convertAvmKeyValue(v, kv)); + } + } else if (v instanceof AvmValueReference) { + if (v.scope instanceof ProgramStateScope) { + const frame = this._runtime.getStackFrame(v.scope.frameIndex); + if (!frame || !(frame instanceof ProgramStackFrame)) { + throw new Error(`Unexpected frame: ${typeof frame}`); + } + let toExpand: algosdk.modelsv2.AvmValue; + if (v.scope.specificState === 'stack') { + toExpand = frame.state.stack[v.key as number]; + } else if (v.scope.specificState === 'scratch') { + toExpand = + frame.state.scratch.get(v.key as number) || + new algosdk.modelsv2.AvmValue({ type: 2 }); + } else { + throw new Error(`Unexpected AvmValueReference scope: ${v.scope}`); + } + variables = this.expandAvmValue(toExpand, args.filter); + } else if ( + v.scope instanceof AppSpecificStateScope && + typeof v.key === 'string' && + v.key.startsWith('0x') + ) { + let toExpand: algosdk.modelsv2.AvmKeyValue; + const state = this._runtime.getAppState(v.scope.appID); + const keyHex = v.key.slice(2); + if (v.scope.scope === 'global') { + const value = state.globalState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + toExpand = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + } else { + throw new Error(`key "${v.key}" not found in global state`); + } + } else if (v.scope.scope === 'local') { + if (typeof v.scope.account === 'undefined') { + throw new Error("this shouldn't happen: " + JSON.stringify(v)); + } else { + const accountState = state.localState.get(v.scope.account); + if (!accountState) { + throw new Error( + `account "${v.scope.account}" not found in local state`, + ); + } + const value = accountState.getHex(keyHex); + if (!value) { + throw new Error( + `key "${v.key}" not found in local state for account "${v.scope.account}"`, + ); + } + toExpand = new algosdk.modelsv2.AvmKeyValue({ + key: Buffer.from(keyHex, 'hex'), + value, + }); + } + } else if (v.scope.scope === 'box') { + const value = state.boxState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + toExpand = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + } else { + throw new Error(`key "${v.key}" not found in box state`); + } + } else { + throw new Error(`Unexpected AppSpecificStateScope scope: ${v.scope}`); + } + variables = this.expandAvmKeyValue(v.scope, toExpand, args.filter); + } + } + + variables = limitArray(variables, args.start, args.count); + + response.body = { + variables, + }; + this.sendResponse(response); + } + + protected async evaluateRequest( + response: DebugProtocol.EvaluateResponse, + args: DebugProtocol.EvaluateArguments, + ): Promise { + let reply: string | undefined; + let rv: DebugProtocol.Variable | undefined = undefined; + + // Note, can use args.context to perform different actions based on where the expression is evaluated + + let result: [AvmValueScope, number | string] | undefined = undefined; + try { + result = evaluateNameToScope(args.expression); + } catch (e) { + reply = (e as Error).message; + } + + if (result) { + const [scope, key] = result; + if (scope instanceof ProgramStateScope) { + if (typeof args.frameId === 'undefined') { + reply = 'frameId required for program state'; + } else { + const scopeWithFrame = new ProgramStateScope( + args.frameId, + scope.specificState, + ); + const frame = this._runtime.getStackFrame(args.frameId); + if (!frame || !(frame instanceof ProgramStackFrame)) { + reply = `Unexpected frame: ${typeof frame}`; + } else { + if (scope.specificState === 'pc') { + rv = { + name: 'pc', + value: frame.state.pc.toString(), + type: 'uint64', + variablesReference: 0, + evaluateName: 'pc', + }; + } else if (scope.specificState === 'stack') { + let index = key as number; + const stackValues = frame.state.stack; + if (index < 0) { + const adjustedIndex = index + stackValues.length; + if (adjustedIndex < 0) { + reply = `stack[${index}] out of range`; + } else { + index = adjustedIndex; + } + } + if (0 <= index && index < stackValues.length) { + rv = this.convertAvmValue( + scopeWithFrame, + stackValues[index], + index, + ); + } else if (index < 0 && stackValues.length + index >= 0) { + rv = this.convertAvmValue( + scopeWithFrame, + stackValues[stackValues.length + index], + index, + ); + } else { + reply = `stack[${index}] out of range`; + } + } else if (scope.specificState === 'scratch') { + const index = key as number; + if (0 <= index && index < 256) { + rv = this.convertAvmValue( + scopeWithFrame, + frame.state.scratch.get(index) || + new algosdk.modelsv2.AvmValue({ type: 2 }), + index, + ); + } else { + reply = `scratch[${index}] out of range`; + } + } + } + } + } else if (typeof key === 'string') { + const state = this._runtime.getAppState(scope.appID); + if (scope.property) { + reply = `cannot evaluate property "${scope.property}"`; + } else if (scope.scope === 'global' && key.startsWith('0x')) { + const keyHex = key.slice(2); + const value = state.globalState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + const kv = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + rv = this.convertAvmKeyValue(scope, kv); + } else { + reply = `key "${key}" not found in global state`; + } + } else if (scope.scope === 'local') { + if (typeof scope.account === 'undefined') { + rv = { + name: key, + value: 'local state', + type: 'object', + variablesReference: this._variableHandles.create( + new AppSpecificStateScope({ + scope: 'local', + appID: scope.appID, + account: key, + }), + ), + namedVariables: 1, // TODO + evaluateName: evaluateNameForScope(scope, key), + }; + } else { + const accountState = state.localState.get(scope.account); + if (!accountState) { + reply = `account "${scope.account}" not found in local state`; + } else if (key.startsWith('0x')) { + const keyHex = key.slice(2); + const value = accountState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + const kv = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + rv = this.convertAvmKeyValue(scope, kv); + } else { + reply = `key "${key}" not found in local state for account "${scope.account}"`; + } + } else { + reply = `cannot evaluate property "${key}"`; + } + } + } else if (scope.scope === 'box' && key.startsWith('0x')) { + const keyHex = key.slice(2); + const value = state.boxState.getHex(keyHex); + if (value) { + const keyBytes = Buffer.from(keyHex, 'hex'); + const kv = new algosdk.modelsv2.AvmKeyValue({ + key: keyBytes, + value, + }); + rv = this.convertAvmKeyValue(scope, kv); + } else { + reply = `key "${key}" not found in box state`; + } + } + } + } + + if (rv) { + response.body = { + result: rv.value, + type: rv.type, + variablesReference: rv.variablesReference, + presentationHint: rv.presentationHint, + }; + } else { + response.body = { + result: reply || `unknown expression: "${args.expression}"`, + variablesReference: 0, + }; + } + + this.sendResponse(response); + } + + protected sourceRequest( + response: DebugProtocol.SourceResponse, + args: DebugProtocol.SourceArguments, + request?: DebugProtocol.Request, + ): void { + const sourceInfo = this._sourceHandles.get(args.sourceReference); + if (typeof sourceInfo !== 'undefined') { + response.body = { + content: sourceInfo.content, + mimeType: sourceInfo.mimeType, + }; + } else { + response.body = { + content: `source not available`, + }; + } + this.sendResponse(response); + } + + protected continueRequest( + response: DebugProtocol.ContinueResponse, + args: DebugProtocol.ContinueArguments, + ): void { + this.executionResumed(); + this._runtime.continue(false); + this.sendResponse(response); + } + + protected reverseContinueRequest( + response: DebugProtocol.ReverseContinueResponse, + args: DebugProtocol.ReverseContinueArguments, + ): void { + this.executionResumed(); + this._runtime.continue(true); + this.sendResponse(response); + } + + protected nextRequest( + response: DebugProtocol.NextResponse, + args: DebugProtocol.NextArguments, + ): void { + this.executionResumed(); + this._runtime.step(false); + this.sendResponse(response); + } + + protected stepBackRequest( + response: DebugProtocol.StepBackResponse, + args: DebugProtocol.StepBackArguments, + ): void { + this.executionResumed(); + this._runtime.step(true); + this.sendResponse(response); + } + + protected stepInTargetsRequest( + response: DebugProtocol.StepInTargetsResponse, + args: DebugProtocol.StepInTargetsArguments, + ) { + const targets = this._runtime.getStepInTargets(args.frameId); + response.body = { + targets: targets.map((t) => { + return { id: t.id, label: t.label }; + }), + }; + this.sendResponse(response); + } + + protected stepInRequest( + response: DebugProtocol.StepInResponse, + args: DebugProtocol.StepInArguments, + ): void { + this.executionResumed(); + this._runtime.stepIn(args.targetId); + this.sendResponse(response); + } + + protected stepOutRequest( + response: DebugProtocol.StepOutResponse, + args: DebugProtocol.StepOutArguments, + ): void { + this.executionResumed(); + this._runtime.stepOut(); + this.sendResponse(response); + } + + private executionResumed(): void { + this._variableHandles.reset(); + this._sourceHandles.reset(); + } + + //---- helpers + + private convertAvmValue( + scope: AvmValueScope, + avmValue: algosdk.modelsv2.AvmValue, + key: number | string, + overrideVariableReference?: boolean, + ): DebugProtocol.Variable { + let namedVariables: number | undefined = undefined; + let indexedVariables: number | undefined = undefined; + let presentationHint: DebugProtocol.VariablePresentationHint | undefined = + undefined; + let makeVariableReference = false; + if (avmValue.type === 1) { + // byte array + const bytes = avmValue.bytes || new Uint8Array(); + namedVariables = 2; + if (isAsciiPrintable(bytes)) { + namedVariables++; + } + indexedVariables = bytes.length; + presentationHint = { + kind: 'data', + attributes: ['rawString'], + }; + makeVariableReference = true; + } + if (typeof overrideVariableReference !== 'undefined') { + makeVariableReference = overrideVariableReference; + } + return { + name: key.toString(), + value: this.avmValueToString(avmValue), + type: avmValue.type === 1 ? 'byte[]' : 'uint64', + variablesReference: makeVariableReference + ? this._variableHandles.create(new AvmValueReference(scope, key)) + : 0, + namedVariables, + indexedVariables, + presentationHint, + evaluateName: evaluateNameForScope(scope, key), + }; + } + + private expandAvmValue( + avmValue: algosdk.modelsv2.AvmValue, + filter?: DebugProtocol.VariablesArguments['filter'], + ): DebugProtocol.Variable[] { + if (avmValue.type !== 1) { + return []; + } + + const bytes = avmValue.bytes || new Uint8Array(); + + const values: DebugProtocol.Variable[] = []; + + if (filter !== 'indexed') { + let formats: BufferEncoding[] = ['hex', 'base64']; + if (isAsciiPrintable(bytes)) { + // TODO: perhaps do this with UTF-8 instead, see https://stackoverflow.com/questions/75108373/how-to-check-if-a-node-js-buffer-contains-valid-utf-8 + formats.push('ascii'); + } + if (bytes.length === 0) { + formats = []; + } + + for (const format of formats) { + values.push({ + name: format, + type: 'string', + value: Buffer.from(bytes).toString(format), + variablesReference: 0, + }); + } + + if (bytes.length === 32) { + values.push({ + name: 'address', + type: 'string', + value: algosdk.encodeAddress(bytes), + variablesReference: 0, + }); + } + + values.push({ + name: 'length', + type: 'int', + value: bytes.length.toString(), + variablesReference: 0, + }); + } + + if (filter !== 'named') { + for (let i = 0; i < bytes.length; i++) { + values.push({ + name: i.toString(), + type: 'uint8', + value: bytes[i].toString(), + variablesReference: 0, + }); + } + } + + return values; + } + + private convertAvmKeyValue( + scope: AvmValueScope, + avmKeyValue: algosdk.modelsv2.AvmKeyValue, + ): DebugProtocol.Variable { + const keyString = + '0x' + Buffer.from(avmKeyValue.key || new Uint8Array()).toString('hex'); + const value = this.convertAvmValue( + scope, + avmKeyValue.value, + keyString, + true, + ); + delete value.indexedVariables; + value.namedVariables = 2; + return value; + } + + private expandAvmKeyValue( + scope: AppSpecificStateScope, + avmKeyValue: algosdk.modelsv2.AvmKeyValue, + filter?: DebugProtocol.VariablesArguments['filter'], + ): DebugProtocol.Variable[] { + if (typeof scope.property === 'undefined') { + if (filter === 'indexed') { + return []; + } + const keyString = + '0x' + Buffer.from(avmKeyValue.key || new Uint8Array()).toString('hex'); + const keyScope = new AppSpecificStateScope({ + scope: scope.scope, + appID: scope.appID, + account: scope.account, + property: 'key', + }); + const valueScope = new AppSpecificStateScope({ + scope: scope.scope, + appID: scope.appID, + account: scope.account, + property: 'value', + }); + const keyVariable = this.convertAvmValue( + keyScope, + new algosdk.modelsv2.AvmValue({ type: 1, bytes: avmKeyValue.key }), + '', + false, + ); + const valueVariable = this.convertAvmValue( + valueScope, + avmKeyValue.value, + '', + false, + ); + const valueHasChildren = + valueVariable.namedVariables || valueVariable.indexedVariables; + return [ + { + name: 'key', + type: keyVariable.type, + value: keyVariable.value, + variablesReference: this._variableHandles.create( + new AvmValueReference(keyScope, keyString), + ), + namedVariables: keyVariable.namedVariables, + indexedVariables: keyVariable.indexedVariables, + presentationHint: keyVariable.presentationHint, + // evaluateName: evaluateNameForScope(keyScope, keyString), + }, + { + name: 'value', + type: keyVariable.type, + value: valueVariable.value, + variablesReference: valueHasChildren + ? this._variableHandles.create( + new AvmValueReference(valueScope, keyString), + ) + : 0, + namedVariables: keyVariable.namedVariables, + indexedVariables: keyVariable.indexedVariables, + presentationHint: keyVariable.presentationHint, + // evaluateName: valueHasChildren ? evaluateNameForScope(valueScope, keyString) : '', + }, + ]; + } + + if (scope.property === 'key') { + const avmKey = new algosdk.modelsv2.AvmValue({ + type: 1, + bytes: avmKeyValue.key, + }); + return this.expandAvmValue(avmKey, filter); + } + + return this.expandAvmValue(avmKeyValue.value, filter); + } + + private avmValueToString(avmValue: algosdk.modelsv2.AvmValue): string { + if (avmValue.type === 1) { + // byte array + const bytes = avmValue.bytes || new Uint8Array(); + return '0x' + Buffer.from(bytes).toString('hex'); + } + // uint64 + const uint = avmValue.uint || 0; + return uint.toString(); + } + + private createSource(filePath: string): Source { + return new Source( + basename(filePath), + this.convertDebuggerPathToClient(filePath), + undefined, + undefined, + 'teal-txn-group-adapter-data', + ); + } + + private createSourceWithContent( + fileName: string, + content: string, + mimeType?: string, + ) { + const id = this._sourceHandles.create({ content, mimeType }); + return new Source(fileName, undefined, id); + } } class ProgramStateScope { - constructor( - public readonly frameIndex: number, - public readonly specificState?: 'pc' | 'stack' | 'scratch' - ) { } - - public scopeString(): string { - if (typeof this.specificState === 'undefined') { - return `program`; - } - return this.specificState; - } + constructor( + public readonly frameIndex: number, + public readonly specificState?: 'pc' | 'stack' | 'scratch', + ) {} + + public scopeString(): string { + if (typeof this.specificState === 'undefined') { + return `program`; + } + return this.specificState; + } } type OnChainStateScope = 'chain' | 'app'; class AppStateScope { - constructor(public readonly appID: number) { } + constructor(public readonly appID: number) {} } class AppSpecificStateScope { - public readonly scope: 'global' | 'local' | 'box'; - public readonly appID: number; - public readonly account?: string; - public readonly property?: 'key' | 'value'; - - constructor({ - scope, - appID, - account, - property - }: { - scope: 'global' | 'local' | 'box', - appID: number, - account?: string, - property?: 'key' | 'value' - }) { - this.scope = scope; - this.appID = appID; - this.account = account; - this.property = property; - } + public readonly scope: 'global' | 'local' | 'box'; + public readonly appID: number; + public readonly account?: string; + public readonly property?: 'key' | 'value'; + + constructor({ + scope, + appID, + account, + property, + }: { + scope: 'global' | 'local' | 'box'; + appID: number; + account?: string; + property?: 'key' | 'value'; + }) { + this.scope = scope; + this.appID = appID; + this.account = account; + this.property = property; + } } type AvmValueScope = ProgramStateScope | AppSpecificStateScope; class AvmValueReference { - constructor( - public readonly scope: AvmValueScope, - public readonly key: number | string - ) { } + constructor( + public readonly scope: AvmValueScope, + public readonly key: number | string, + ) {} } -function evaluateNameForScope(scope: AvmValueScope, key: number | string): string { - if (scope instanceof ProgramStateScope) { - return `${scope.scopeString()}[${key}]`; - } - if (scope.scope === 'local') { - if (typeof scope.account === 'undefined') { - return `app[${scope.appID}].local[${key}]`; - } - return `app[${scope.appID}].local[${scope.account}][${key}]`; - } - return `app[${scope.appID}].${scope.scope}[${key}]${scope.property ? '.' + scope.property : ''}`; +function evaluateNameForScope( + scope: AvmValueScope, + key: number | string, +): string { + if (scope instanceof ProgramStateScope) { + return `${scope.scopeString()}[${key}]`; + } + if (scope.scope === 'local') { + if (typeof scope.account === 'undefined') { + return `app[${scope.appID}].local[${key}]`; + } + return `app[${scope.appID}].local[${scope.account}][${key}]`; + } + return `app[${scope.appID}].${scope.scope}[${key}]${ + scope.property ? '.' + scope.property : '' + }`; } function evaluateNameToScope(name: string): [AvmValueScope, number | string] { - if (name === 'pc') { - return [new ProgramStateScope(-1, 'pc'), 0]; - } - const stackMatches = /^stack\[(-?\d+)\]$/.exec(name); - if (stackMatches) { - return [new ProgramStateScope(-1, 'stack'), parseInt(stackMatches[1], 10)]; - } - const scratchMatches = /^scratch\[(\d+)\]$/.exec(name); - if (scratchMatches) { - return [new ProgramStateScope(-1, 'scratch'), parseInt(scratchMatches[1], 10)]; - } - const appMatches = /^app\[(\d+)\]\.(global|box)\[(0[xX][0-9a-fA-F]+)\](?:\.(key|value))?$/.exec(name); - if (appMatches) { - const scope = appMatches[2]; - if (scope !== 'global' && scope !== 'box') { - throw new Error(`Unexpected app scope: ${scope}`); - } - const property = appMatches.length > 4 ? appMatches[4] : undefined; - if (typeof property !== 'undefined' && property !== 'key' && property !== 'value') { - throw new Error(`Unexpected app property: ${property}`); - } - const newScope = new AppSpecificStateScope({ - scope: scope, - appID: parseInt(appMatches[1], 10), - property - }); - return [newScope, appMatches[3]]; - } - const appLocalMatches = /^app\[(\d+)\]\.local\[([A-Z2-7]{58})\](?:\[(0[xX][0-9a-fA-F]+)\](?:\.(key|value))?)?$/.exec(name); - if (appLocalMatches) { - const property = appLocalMatches.length > 4 ? appLocalMatches[4] : undefined; - if (typeof property !== 'undefined' && property !== 'key' && property !== 'value') { - throw new Error(`Unexpected app property: ${property}`); - } - try { - algosdk.decodeAddress(appLocalMatches[2]); // ensure valid address - } catch { - throw new Error(`Invalid address: ${appLocalMatches[2]}:`); - } - let account: string | undefined; - let key: string; - if (appLocalMatches.length > 3 && typeof appLocalMatches[3] !== 'undefined') { - account = appLocalMatches[2]; - key = appLocalMatches[3]; - } else { - account = undefined; - key = appLocalMatches[2]; - } - const newScope = new AppSpecificStateScope({ - scope: 'local', - appID: parseInt(appLocalMatches[1], 10), - account, - property - }); - return [newScope, key]; - } - throw new Error(`Unexpected expression: ${name}`); + if (name === 'pc') { + return [new ProgramStateScope(-1, 'pc'), 0]; + } + const stackMatches = /^stack\[(-?\d+)\]$/.exec(name); + if (stackMatches) { + return [new ProgramStateScope(-1, 'stack'), parseInt(stackMatches[1], 10)]; + } + const scratchMatches = /^scratch\[(\d+)\]$/.exec(name); + if (scratchMatches) { + return [ + new ProgramStateScope(-1, 'scratch'), + parseInt(scratchMatches[1], 10), + ]; + } + const appMatches = + /^app\[(\d+)\]\.(global|box)\[(0[xX][0-9a-fA-F]+)\](?:\.(key|value))?$/.exec( + name, + ); + if (appMatches) { + const scope = appMatches[2]; + if (scope !== 'global' && scope !== 'box') { + throw new Error(`Unexpected app scope: ${scope}`); + } + const property = appMatches.length > 4 ? appMatches[4] : undefined; + if ( + typeof property !== 'undefined' && + property !== 'key' && + property !== 'value' + ) { + throw new Error(`Unexpected app property: ${property}`); + } + const newScope = new AppSpecificStateScope({ + scope: scope, + appID: parseInt(appMatches[1], 10), + property, + }); + return [newScope, appMatches[3]]; + } + const appLocalMatches = + /^app\[(\d+)\]\.local\[([A-Z2-7]{58})\](?:\[(0[xX][0-9a-fA-F]+)\](?:\.(key|value))?)?$/.exec( + name, + ); + if (appLocalMatches) { + const property = + appLocalMatches.length > 4 ? appLocalMatches[4] : undefined; + if ( + typeof property !== 'undefined' && + property !== 'key' && + property !== 'value' + ) { + throw new Error(`Unexpected app property: ${property}`); + } + try { + algosdk.decodeAddress(appLocalMatches[2]); // ensure valid address + } catch { + throw new Error(`Invalid address: ${appLocalMatches[2]}:`); + } + let account: string | undefined; + let key: string; + if ( + appLocalMatches.length > 3 && + typeof appLocalMatches[3] !== 'undefined' + ) { + account = appLocalMatches[2]; + key = appLocalMatches[3]; + } else { + account = undefined; + key = appLocalMatches[2]; + } + const newScope = new AppSpecificStateScope({ + scope: 'local', + appID: parseInt(appLocalMatches[1], 10), + account, + property, + }); + return [newScope, key]; + } + throw new Error(`Unexpected expression: ${name}`); } diff --git a/src/debugAdapter/traceReplayEngine.ts b/src/debugAdapter/traceReplayEngine.ts index 727a159..81bdb9d 100644 --- a/src/debugAdapter/traceReplayEngine.ts +++ b/src/debugAdapter/traceReplayEngine.ts @@ -1,722 +1,852 @@ import * as algosdk from 'algosdk'; import { AppState } from './appState'; -import { ByteArrayMap, TEALDebuggingAssets, TxnGroupSourceDescriptor } from './utils'; +import { + ByteArrayMap, + TEALDebuggingAssets, + TxnGroupSourceDescriptor, +} from './utils'; export class TraceReplayEngine { + public debugAssets: TEALDebuggingAssets; + public programHashToSource: ByteArrayMap< + TxnGroupSourceDescriptor | undefined + > = new ByteArrayMap(); + public framePaths: number[][] = []; - public debugAssets: TEALDebuggingAssets; - public programHashToSource: ByteArrayMap = new ByteArrayMap(); - public framePaths: number[][] = []; - - public initialAppState = new Map(); - public currentAppState = new Map(); + public initialAppState = new Map(); + public currentAppState = new Map(); - public stack: TraceReplayStackFrame[] = []; + public stack: TraceReplayStackFrame[] = []; - constructor(debugAssets: TEALDebuggingAssets) { - this.debugAssets = debugAssets; + constructor(debugAssets: TEALDebuggingAssets) { + this.debugAssets = debugAssets; - const { simulateResponse } = this.debugAssets; + const { simulateResponse } = this.debugAssets; - for (const initialAppState of simulateResponse.initialStates?.appInitialStates || []) { - this.initialAppState.set(Number(initialAppState.id), AppState.fromAppInitialState(initialAppState)); - } - - for (let groupIndex = 0; groupIndex < simulateResponse.txnGroups.length; groupIndex++) { - const group = simulateResponse.txnGroups[groupIndex]; - - this.framePaths.push([groupIndex]); - - for (let txnIndex = 0; txnIndex < group.txnResults.length; txnIndex++) { - this.setupTxnTrace(groupIndex, txnIndex); - } - } - - this.resetCurrentAppState(); - this.setStartingStack(); - } - - private setStartingStack() { - const { simulateResponse } = this.debugAssets; - this.stack = [ - new TopLevelTransactionGroupsFrame(this, simulateResponse) - ]; - if (simulateResponse.txnGroups.length === 1) { - // If only a single group, get rid of the top-level frame - this.forward(); - this.stack.shift(); - } - } - - private resetCurrentAppState() { - this.currentAppState = new Map( - Array.from(this.initialAppState.entries(), ([key, value]) => [key, value.clone()]) - ); + for (const initialAppState of simulateResponse.initialStates + ?.appInitialStates || []) { + this.initialAppState.set( + Number(initialAppState.id), + AppState.fromAppInitialState(initialAppState), + ); } - private setupTxnTrace(groupIndex: number, txnIndex: number) { - const txnPath = [groupIndex, txnIndex]; - this.framePaths.push(txnPath); + for ( + let groupIndex = 0; + groupIndex < simulateResponse.txnGroups.length; + groupIndex++ + ) { + const group = simulateResponse.txnGroups[groupIndex]; - const txn = this.debugAssets.simulateResponse.txnGroups[groupIndex].txnResults[txnIndex]; - const trace = txn.execTrace; - if (!trace) { - // Probably not an app call txn - return; - } - if (trace.logicSigTrace) { - this.fetchProgramSourceInfo(trace.logicSigHash!); - } - visitAppTrace(txnPath, txn.txnResult, trace, (path, programHash, txnInfo, opcodes) => { - this.framePaths.push(path); - this.fetchProgramSourceInfo(programHash); - - let appID = txnInfo.applicationIndex || txnInfo.txn.txn.apid; - if (typeof appID === 'undefined') { - throw new Error(`No appID for txn at path ${path}`); - } else { - appID = Number(appID); - } - - let initialAppState = this.initialAppState.get(appID); - if (typeof initialAppState === 'undefined') { - initialAppState = new AppState(); - this.initialAppState.set(appID, initialAppState); - } + this.framePaths.push([groupIndex]); - for (const opcode of opcodes) { - for (const stateChange of opcode.stateChanges || []) { - if (stateChange.appStateType === 'l') { - const account = stateChange.account!; - if (!initialAppState.localState.has(account)) { - initialAppState.localState.set(account, new ByteArrayMap()); - } - } - } - } - }); + for (let txnIndex = 0; txnIndex < group.txnResults.length; txnIndex++) { + this.setupTxnTrace(groupIndex, txnIndex); + } } - private fetchProgramSourceInfo(programHash: Uint8Array) { - if (this.programHashToSource.has(programHash)) { - return; - } - const sourceDescriptor = this.debugAssets.txnGroupDescriptorList.findByHash(programHash); - this.programHashToSource.set(programHash, sourceDescriptor); + this.resetCurrentAppState(); + this.setStartingStack(); + } + + private setStartingStack() { + const { simulateResponse } = this.debugAssets; + this.stack = [new TopLevelTransactionGroupsFrame(this, simulateResponse)]; + if (simulateResponse.txnGroups.length === 1) { + // If only a single group, get rid of the top-level frame + this.forward(); + this.stack.shift(); } - - public currentFrame(): TraceReplayStackFrame { - return this.stack[this.stack.length - 1]; + } + + private resetCurrentAppState() { + this.currentAppState = new Map( + Array.from(this.initialAppState.entries(), ([key, value]) => [ + key, + value.clone(), + ]), + ); + } + + private setupTxnTrace(groupIndex: number, txnIndex: number) { + const txnPath = [groupIndex, txnIndex]; + this.framePaths.push(txnPath); + + const txn = + this.debugAssets.simulateResponse.txnGroups[groupIndex].txnResults[ + txnIndex + ]; + const trace = txn.execTrace; + if (!trace) { + // Probably not an app call txn + return; } - - public forward(): boolean { - let length: number; - do { - length = this.stack.length; - this.currentFrame().forward(this.stack); - if (this.stack.length === 0) { - return false; - } - } while (this.stack.length < length); - return true; - } - - public backward(): boolean { - let length: number; - do { - length = this.stack.length; - this.currentFrame().backward(this.stack); - if (this.stack.length === 0) { - this.setStartingStack(); - return false; + if (trace.logicSigTrace) { + this.fetchProgramSourceInfo(trace.logicSigHash!); + } + visitAppTrace( + txnPath, + txn.txnResult, + trace, + (path, programHash, txnInfo, opcodes) => { + this.framePaths.push(path); + this.fetchProgramSourceInfo(programHash); + + let appID = txnInfo.applicationIndex || txnInfo.txn.txn.apid; + if (typeof appID === 'undefined') { + throw new Error(`No appID for txn at path ${path}`); + } else { + appID = Number(appID); + } + + let initialAppState = this.initialAppState.get(appID); + if (typeof initialAppState === 'undefined') { + initialAppState = new AppState(); + this.initialAppState.set(appID, initialAppState); + } + + for (const opcode of opcodes) { + for (const stateChange of opcode.stateChanges || []) { + if (stateChange.appStateType === 'l') { + const account = stateChange.account!; + if (!initialAppState.localState.has(account)) { + initialAppState.localState.set(account, new ByteArrayMap()); + } } - } while (this.stack.length < length); - return true; + } + } + }, + ); + } + + private fetchProgramSourceInfo(programHash: Uint8Array) { + if (this.programHashToSource.has(programHash)) { + return; } + const sourceDescriptor = + this.debugAssets.txnGroupDescriptorList.findByHash(programHash); + this.programHashToSource.set(programHash, sourceDescriptor); + } + + public currentFrame(): TraceReplayStackFrame { + return this.stack[this.stack.length - 1]; + } + + public forward(): boolean { + let length: number; + do { + length = this.stack.length; + this.currentFrame().forward(this.stack); + if (this.stack.length === 0) { + return false; + } + } while (this.stack.length < length); + return true; + } + + public backward(): boolean { + let length: number; + do { + length = this.stack.length; + this.currentFrame().backward(this.stack); + if (this.stack.length === 0) { + this.setStartingStack(); + return false; + } + } while (this.stack.length < length); + return true; + } } function visitAppTrace( + path: number[], + txnInfo: algosdk.modelsv2.PendingTransactionResponse, + trace: algosdk.modelsv2.SimulationTransactionExecTrace, + visitor: ( path: number[], + programHash: Uint8Array, txnInfo: algosdk.modelsv2.PendingTransactionResponse, - trace: algosdk.modelsv2.SimulationTransactionExecTrace, - visitor: ( - path: number[], - programHash: Uint8Array, - txnInfo: algosdk.modelsv2.PendingTransactionResponse, - opcodes: algosdk.modelsv2.SimulationOpcodeTraceUnit[] - ) => void + opcodes: algosdk.modelsv2.SimulationOpcodeTraceUnit[], + ) => void, ) { - if (trace.approvalProgramTrace) { - visitor(path, trace.approvalProgramHash!, txnInfo, trace.approvalProgramTrace); - } - if (trace.clearStateProgramTrace) { - visitor(path, trace.clearStateProgramHash!, txnInfo, trace.clearStateProgramTrace); - } - if (trace.innerTrace) { - for (let i = 0; i < trace.innerTrace.length; i++) { - const innerInfo = txnInfo.innerTxns![i]; - const innerTrace = trace.innerTrace[i]; - const innerPath = path.slice(); - innerPath.push(i); - visitAppTrace(innerPath, innerInfo, innerTrace, visitor); - } + if (trace.approvalProgramTrace) { + visitor( + path, + trace.approvalProgramHash!, + txnInfo, + trace.approvalProgramTrace, + ); + } + if (trace.clearStateProgramTrace) { + visitor( + path, + trace.clearStateProgramHash!, + txnInfo, + trace.clearStateProgramTrace, + ); + } + if (trace.innerTrace) { + for (let i = 0; i < trace.innerTrace.length; i++) { + const innerInfo = txnInfo.innerTxns![i]; + const innerTrace = trace.innerTrace[i]; + const innerPath = path.slice(); + innerPath.push(i); + visitAppTrace(innerPath, innerInfo, innerTrace, visitor); } + } } export interface FrameSource { - name: string; - path?: string; - content?: string; - contentMimeType?: string; + name: string; + path?: string; + content?: string; + contentMimeType?: string; } export interface FrameSourceLocation { - line: number; - endLine?: number; - column?: number; - endColumn?: number; + line: number; + endLine?: number; + column?: number; + endColumn?: number; } export abstract class TraceReplayStackFrame { + constructor(protected readonly engine: TraceReplayEngine) {} - constructor(protected readonly engine: TraceReplayEngine) { } + public abstract name(): string; + public abstract sourceFile(): FrameSource; + public abstract sourceLocation(): FrameSourceLocation; - public abstract name(): string; - public abstract sourceFile(): FrameSource; - public abstract sourceLocation(): FrameSourceLocation; - - public abstract forward(stack: TraceReplayStackFrame[]): void; - public abstract backward(stack: TraceReplayStackFrame[]): void; + public abstract forward(stack: TraceReplayStackFrame[]): void; + public abstract backward(stack: TraceReplayStackFrame[]): void; } export class TopLevelTransactionGroupsFrame extends TraceReplayStackFrame { - - private index: number = 0; - private txnGroupDone: boolean = false; - - constructor( - engine: TraceReplayEngine, - private readonly response: algosdk.modelsv2.SimulateResponse - ) { - super(engine); + private index: number = 0; + private txnGroupDone: boolean = false; + + constructor( + engine: TraceReplayEngine, + private readonly response: algosdk.modelsv2.SimulateResponse, + ) { + super(engine); + } + + public name(): string { + return `group ${this.index}`; + } + + public sourceFile(): FrameSource { + const individualGroups = this.response.txnGroups.map((group) => + group.txnResults.map( + (txnResult) => txnResult.txnResult.get_obj_for_encoding().txn, + ), + ); + return { + name: `transaction-groups.json`, + content: JSON.stringify(individualGroups, null, 2), + contentMimeType: 'application/json', + }; + } + + public sourceLocation(): FrameSourceLocation { + let lineOffset = 1; // For opening bracket + for (let i = 0; i < this.index; i++) { + for (const txnResult of this.response.txnGroups[i].txnResults) { + const displayedTxn = txnResult.txnResult.get_obj_for_encoding().txn; + lineOffset += JSON.stringify(displayedTxn, null, 2).split('\n').length; + } + lineOffset += 2; // For opening and closing brackets } - - public name(): string { - return `group ${this.index}`; + let lineCount = 2; // For opening and closing brackets + for (const txnResult of this.response.txnGroups[this.index].txnResults) { + const displayedTxn = txnResult.txnResult.get_obj_for_encoding().txn; + lineCount += JSON.stringify(displayedTxn, null, 2).split('\n').length; } - - public sourceFile(): FrameSource { - const individualGroups = this.response.txnGroups.map(group => - group.txnResults.map(txnResult => txnResult.txnResult.get_obj_for_encoding().txn) - ); - return { - name: `transaction-groups.json`, - content: JSON.stringify(individualGroups, null, 2), - contentMimeType: 'application/json' - }; + return { + line: lineOffset, + endLine: lineOffset + lineCount, + }; + } + + public forward(stack: TraceReplayStackFrame[]): void { + if (!this.txnGroupDone) { + stack.push(this.frameForIndex(this.index)); + this.txnGroupDone = true; + return; } - - public sourceLocation(): FrameSourceLocation { - let lineOffset = 1; // For opening bracket - for (let i = 0; i < this.index; i++) { - for (const txnResult of this.response.txnGroups[i].txnResults) { - const displayedTxn = txnResult.txnResult.get_obj_for_encoding().txn; - // + 2 is for opening and closing brackets - lineOffset += JSON.stringify(displayedTxn, null, 2).split('\n').length; - } - lineOffset += 2; // For opening and closing brackets - } - let lineCount = 2; // For opening and closing brackets - for (const txnResult of this.response.txnGroups[this.index].txnResults) { - const displayedTxn = txnResult.txnResult.get_obj_for_encoding().txn; - // + 2 is for opening and closing brackets - lineCount += JSON.stringify(displayedTxn, null, 2).split('\n').length; - } - return { - line: lineOffset, - endLine: lineOffset + lineCount + 1, - }; + if (this.index + 1 < this.response.txnGroups.length) { + this.index++; + this.txnGroupDone = false; + return; } - - public forward(stack: TraceReplayStackFrame[]): void { - if (!this.txnGroupDone) { - stack.push(this.frameForIndex(this.index)); - this.txnGroupDone = true; - return; - } - if (this.index + 1 < this.response.txnGroups.length) { - this.index++; - this.txnGroupDone = false; - return; - } - stack.pop(); + stack.pop(); + } + + private frameForIndex(index: number): TransactionStackFrame { + const txnInfos: algosdk.modelsv2.PendingTransactionResponse[] = []; + const txnTraces: Array< + algosdk.modelsv2.SimulationTransactionExecTrace | undefined + > = []; + for (const { txnResult, execTrace } of this.response.txnGroups[index] + .txnResults) { + txnInfos.push(txnResult); + txnTraces.push(execTrace); } - - private frameForIndex(index: number): TransactionStackFrame { - const txnInfos: algosdk.modelsv2.PendingTransactionResponse[] = []; - const txnTraces: Array = []; - for (const { txnResult, execTrace } of this.response.txnGroups[index].txnResults) { - txnInfos.push(txnResult); - txnTraces.push(execTrace); - } - const txnGroupFrame = new TransactionStackFrame(this.engine, [index], txnInfos, txnTraces); - return txnGroupFrame; + const txnGroupFrame = new TransactionStackFrame( + this.engine, + [index], + txnInfos, + txnTraces, + ); + return txnGroupFrame; + } + + public backward(stack: TraceReplayStackFrame[]): void { + if (this.txnGroupDone) { + this.txnGroupDone = false; + return; } - - public backward(stack: TraceReplayStackFrame[]): void { - if (this.txnGroupDone) { - this.txnGroupDone = false; - return; - } - if (this.index === 0) { - stack.pop(); - return; - } - this.index--; - this.txnGroupDone = true; + if (this.index === 0) { + stack.pop(); + return; } + this.index--; + this.txnGroupDone = true; + } } interface TransactionSourceLocation { - line: number, - lineEnd?: number, - lsigLocation?: { - line: number, - lineEnd?: number, - }, - appLocation?: { - line: number, - lineEnd?: number, - }, + line: number; + lineEnd?: number; + lsigLocation?: { + line: number; + lineEnd?: number; + }; + appLocation?: { + line: number; + lineEnd?: number; + }; +} + +enum ProgramStatus { + /* eslint-disable @typescript-eslint/naming-convention */ + NOT_STARTED, + STARTING, + DONE, + /* eslint-enable @typescript-eslint/naming-convention */ } export class TransactionStackFrame extends TraceReplayStackFrame { + private txnIndex: number = 0; + private logicSigStatus: ProgramStatus = ProgramStatus.DONE; + private appStatus: ProgramStatus = ProgramStatus.DONE; + + private sourceContent: string; + private sourceLocations: TransactionSourceLocation[] = []; + + constructor( + engine: TraceReplayEngine, + private readonly txnPath: number[], + private readonly txnInfos: algosdk.modelsv2.PendingTransactionResponse[], + private readonly txnTraces: Array< + algosdk.modelsv2.SimulationTransactionExecTrace | undefined + >, + ) { + super(engine); + + const firstTrace = txnTraces[0]; + if (firstTrace) { + if (firstTrace.logicSigTrace) { + this.logicSigStatus = ProgramStatus.NOT_STARTED; + } + if ( + firstTrace.approvalProgramTrace || + firstTrace.clearStateProgramTrace + ) { + this.appStatus = ProgramStatus.NOT_STARTED; + } + } - private txnIndex: number = 0; - private preambleDone: boolean = false; - private logicSigDone: boolean; - private appDone: boolean; - - private sourceContent: string; - private sourceLocations: TransactionSourceLocation[] = []; - - constructor( - engine: TraceReplayEngine, - private readonly txnPath: number[], - private readonly txnInfos: algosdk.modelsv2.PendingTransactionResponse[], - private readonly txnTraces: Array - ) { - super(engine); - this.logicSigDone = true; - this.appDone = true; - - const firstTrace = txnTraces[0]; - if (firstTrace) { - if (firstTrace.logicSigTrace) { - this.logicSigDone = false; + const individualTxns = this.txnInfos.map( + (txnInfo) => txnInfo.get_obj_for_encoding().txn, + ); + this.sourceContent = JSON.stringify(individualTxns, null, 2); + let lineOffset = 1; // For opening bracket + for (let i = 0; i < this.txnInfos.length; i++) { + const txnInfo = this.txnInfos[i]; + const txnTrace = this.txnTraces[i]; + const displayedTxn = txnInfo.get_obj_for_encoding().txn; + const displayTxnLines = JSON.stringify(displayedTxn, null, 2).split('\n'); + const sourceLocation: TransactionSourceLocation = { + line: lineOffset, + lineEnd: lineOffset + displayTxnLines.length, + }; + if (txnTrace) { + if (txnTrace.logicSigTrace) { + let lsigLine: number | undefined = undefined; + for (let i = 0; i < displayTxnLines.length; i++) { + const line = displayTxnLines[i]; + if ( + typeof lsigLine === 'undefined' && + line.match(/^\s*"lsig":\s*{\s*$/) + ) { + lsigLine = lineOffset + i; + continue; } - if (firstTrace.approvalProgramTrace || firstTrace.clearStateProgramTrace) { - this.appDone = false; + } + sourceLocation.lsigLocation = { + // Default to lineOffset + 1 if no lsig is present + line: lsigLine ?? lineOffset + 1, + }; + } + if (txnTrace.approvalProgramTrace || txnTrace.clearStateProgramTrace) { + let appIdLine: number | undefined = undefined; + let approvalProgramLine: number | undefined = undefined; + for (let i = 0; i < displayTxnLines.length; i++) { + const line = displayTxnLines[i]; + if (line.match(/^\s*"apid":\s*\d+,\s*$/)) { + appIdLine = lineOffset + i; + // Break here, this is the ideal result + break; } - } - - const individualTxns = this.txnInfos.map(txnInfo => txnInfo.get_obj_for_encoding().txn); - this.sourceContent = JSON.stringify(individualTxns, null, 2); - let lineOffset = 1; // For opening bracket - for (let i = 0; i < this.txnInfos.length; i++) { - const txnInfo = this.txnInfos[i]; - const txnTrace = this.txnTraces[i]; - const displayedTxn = txnInfo.get_obj_for_encoding().txn; - const displayTxnLines = JSON.stringify(displayedTxn, null, 2).split('\n'); - const sourceLocation: TransactionSourceLocation = { - line: lineOffset, - lineEnd: lineOffset + displayTxnLines.length + 1, - }; - if (txnTrace) { - if (txnTrace.logicSigTrace) { - // TODO: lsigLocation - } - if (txnTrace.approvalProgramTrace || txnTrace.clearStateProgramTrace) { - let appIdLine: number | undefined = undefined; - let approvalProgramLine: number | undefined = undefined; - for (let i = 0; i < displayTxnLines.length; i++) { - const line = displayTxnLines[i]; - if (line.match(/^\s*"apid":\s*\d+,\s*$/)) { - appIdLine = lineOffset + i; - // Break here, this is the ideal result - break; - } - if (line.match(/^\s*"apap":\s*"[A-Za-z0-9+\/=]*",\s*$/)) { - // Show approval program if no app ID is present (during create) - approvalProgramLine = lineOffset + i; - // It's possible that this txn can have an approval program and an appID - // (i.e. during an update), so don't break yet. - } - } - sourceLocation.appLocation = { - // Default to lineOffset + 1 if no appID or approval program is present - line: appIdLine ?? approvalProgramLine ?? lineOffset + 1, - }; - } + if (line.match(/^\s*"apap":\s*"[A-Za-z0-9+/=]*",\s*$/)) { + // Show approval program if no app ID is present (during create) + approvalProgramLine = lineOffset + i; + // It's possible that this txn can have an approval program and an appID + // (i.e. during an update), so don't break yet. } - this.sourceLocations.push(sourceLocation); - lineOffset += displayTxnLines.length; - } - } - - public name(): string { - return `${this.txnPath.length > 1 ? 'inner ' : ''}transaction ${this.txnIndex}`; + } + sourceLocation.appLocation = { + // Default to lineOffset + 1 if no appID or approval program is present + line: appIdLine ?? approvalProgramLine ?? lineOffset + 1, + }; + } + } + this.sourceLocations.push(sourceLocation); + lineOffset += displayTxnLines.length; } - - public sourceFile(): FrameSource { - return { - name: `${this.txnPath.length > 1 ? 'inner-' : ''}transaction-group-${this.txnPath.join('-')}.json`, - content: this.sourceContent, - contentMimeType: 'application/json', + } + + public name(): string { + return `${this.txnPath.length > 1 ? 'inner ' : ''}transaction ${ + this.txnIndex + }`; + } + + public sourceFile(): FrameSource { + return { + name: `${ + this.txnPath.length > 1 ? 'inner-' : '' + }transaction-group-${this.txnPath.join('-')}.json`, + content: this.sourceContent, + contentMimeType: 'application/json', + }; + } + + public sourceLocation(): FrameSourceLocation { + const sourceLocation = this.sourceLocations[this.txnIndex]; + let frameSourceLocation: FrameSourceLocation = { + line: sourceLocation.line, + endLine: sourceLocation.lineEnd, + }; + if (this.logicSigStatus === ProgramStatus.STARTING) { + if (sourceLocation.lsigLocation) { + frameSourceLocation = { + line: sourceLocation.lsigLocation.line, + endLine: sourceLocation.lsigLocation.lineEnd, }; - } - - public sourceLocation(): FrameSourceLocation { - const sourceLocation = this.sourceLocations[this.txnIndex]; - let frameSourceLocation: FrameSourceLocation = { - line: sourceLocation.line, - endLine: sourceLocation.lineEnd, + } + } else if (this.appStatus === ProgramStatus.STARTING) { + if (sourceLocation.appLocation) { + frameSourceLocation = { + line: sourceLocation.appLocation.line, + endLine: sourceLocation.appLocation.lineEnd, }; - if (this.preambleDone) { - if (!this.logicSigDone) { - if (sourceLocation.lsigLocation) { - frameSourceLocation = { - line: sourceLocation.lsigLocation.line, - endLine: sourceLocation.lsigLocation.lineEnd, - }; - } - } else if (!this.appDone) { - if (sourceLocation.appLocation) { - frameSourceLocation = { - line: sourceLocation.appLocation.line, - endLine: sourceLocation.appLocation.lineEnd, - }; - } - } - } - return frameSourceLocation; + } } - - public forward(stack: TraceReplayStackFrame[]): void { - const currentTxnTrace = this.txnTraces[this.txnIndex]; - const currentTxnInfo = this.txnInfos[this.txnIndex]; - if (!this.preambleDone && (!this.logicSigDone || !this.appDone)) { - this.preambleDone = true; - return; - } - if (!this.logicSigDone && currentTxnTrace) { - const logicSigFrame = new ProgramStackFrame( - this.engine, - this.txnPath, - 'logic sig', - currentTxnTrace.logicSigHash!, - currentTxnTrace.logicSigTrace!, - currentTxnTrace, - currentTxnInfo, - ); - this.logicSigDone = true; - stack.push(logicSigFrame); - return; - } - if (!this.appDone && currentTxnTrace) { - let appFrame: ProgramStackFrame; - if (currentTxnTrace.approvalProgramTrace) { - appFrame = new ProgramStackFrame( - this.engine, - this.txnPath, - 'approval', - currentTxnTrace.approvalProgramHash!, - currentTxnTrace.approvalProgramTrace!, - currentTxnTrace, - currentTxnInfo, - ); - } else { - appFrame = new ProgramStackFrame( - this.engine, - this.txnPath, - 'clear state', - currentTxnTrace.clearStateProgramHash!, - currentTxnTrace.clearStateProgramTrace!, - currentTxnTrace, - currentTxnInfo, - ); - } - this.appDone = true; - stack.push(appFrame); - return; - } - if (this.txnIndex + 1 < this.txnTraces.length) { - this.txnIndex++; - const nextTrace = this.txnTraces[this.txnIndex]; - this.preambleDone = false; - if (nextTrace) { - this.logicSigDone = nextTrace.logicSigTrace ? false : true; - this.appDone = nextTrace.approvalProgramTrace || nextTrace.clearStateProgramTrace ? false : true; - } else { - this.logicSigDone = true; - this.appDone = true; - } - return; - } - stack.pop(); + return frameSourceLocation; + } + + public forward(stack: TraceReplayStackFrame[]): void { + const currentTxnTrace = this.txnTraces[this.txnIndex]; + const currentTxnInfo = this.txnInfos[this.txnIndex]; + if (this.logicSigStatus === ProgramStatus.NOT_STARTED) { + this.logicSigStatus = ProgramStatus.STARTING; + return; } - - public backward(stack: TraceReplayStackFrame[]): void { - const currentTrace = this.txnTraces[this.txnIndex]; - if (currentTrace) { - if ((currentTrace.approvalProgramTrace || currentTrace.clearStateProgramTrace) && this.appDone) { - this.appDone = false; - return; - } - if (currentTrace.logicSigTrace && this.logicSigDone) { - this.logicSigDone = false; - return; - } - } - if (this.preambleDone) { - this.preambleDone = false; - return; - } - if (this.txnIndex === 0) { - stack.pop(); - return; - } - this.txnIndex--; - this.preambleDone = true; - this.logicSigDone = true; - this.appDone = true; - this.backward(stack); + if (this.logicSigStatus === ProgramStatus.STARTING && currentTxnTrace) { + const logicSigFrame = new ProgramStackFrame( + this.engine, + this.txnPath, + 'logic sig', + currentTxnTrace.logicSigHash!, + currentTxnTrace.logicSigTrace!, + currentTxnTrace, + currentTxnInfo, + ); + this.logicSigStatus = ProgramStatus.DONE; + stack.push(logicSigFrame); + return; + } + if (this.appStatus === ProgramStatus.NOT_STARTED) { + this.appStatus = ProgramStatus.STARTING; + return; + } + if (this.appStatus === ProgramStatus.STARTING && currentTxnTrace) { + let appFrame: ProgramStackFrame; + if (currentTxnTrace.approvalProgramTrace) { + appFrame = new ProgramStackFrame( + this.engine, + this.txnPath, + 'approval', + currentTxnTrace.approvalProgramHash!, + currentTxnTrace.approvalProgramTrace!, + currentTxnTrace, + currentTxnInfo, + ); + } else { + appFrame = new ProgramStackFrame( + this.engine, + this.txnPath, + 'clear state', + currentTxnTrace.clearStateProgramHash!, + currentTxnTrace.clearStateProgramTrace!, + currentTxnTrace, + currentTxnInfo, + ); + } + this.appStatus = ProgramStatus.DONE; + stack.push(appFrame); + return; + } + if (this.txnIndex + 1 < this.txnTraces.length) { + this.txnIndex++; + const nextTrace = this.txnTraces[this.txnIndex]; + if (nextTrace) { + this.logicSigStatus = nextTrace.logicSigTrace + ? ProgramStatus.NOT_STARTED + : ProgramStatus.DONE; + this.appStatus = + nextTrace.approvalProgramTrace || nextTrace.clearStateProgramTrace + ? ProgramStatus.NOT_STARTED + : ProgramStatus.DONE; + } else { + this.logicSigStatus = ProgramStatus.DONE; + this.appStatus = ProgramStatus.DONE; + } + return; + } + stack.pop(); + } + + public backward(stack: TraceReplayStackFrame[]): void { + const currentTrace = this.txnTraces[this.txnIndex]; + if (currentTrace) { + if ( + currentTrace.approvalProgramTrace || + currentTrace.clearStateProgramTrace + ) { + if (this.appStatus === ProgramStatus.DONE) { + this.appStatus = ProgramStatus.STARTING; + return; + } + if (this.appStatus === ProgramStatus.STARTING) { + this.appStatus = ProgramStatus.NOT_STARTED; + // Need to unwind the forward call that is implicit when the app program frame + // is popped + this.backward(stack); + return; + } + } + if (currentTrace.logicSigTrace) { + if (this.logicSigStatus === ProgramStatus.DONE) { + this.logicSigStatus = ProgramStatus.STARTING; + return; + } + if (this.logicSigStatus === ProgramStatus.STARTING) { + this.logicSigStatus = ProgramStatus.NOT_STARTED; + return; + } + } } + if (this.txnIndex === 0) { + stack.pop(); + return; + } + this.txnIndex--; + this.logicSigStatus = ProgramStatus.DONE; + this.appStatus = ProgramStatus.DONE; + const previousTrace = this.txnTraces[this.txnIndex]; + if ( + previousTrace?.approvalProgramHash || + previousTrace?.clearStateProgramHash || + previousTrace?.logicSigHash + ) { + // Need to step back on the app or lsig status + this.backward(stack); + } + } } export interface ProgramState { - pc: number, - stack: algosdk.modelsv2.AvmValue[], - scratch: Map + pc: number; + stack: algosdk.modelsv2.AvmValue[]; + scratch: Map; } export class ProgramStackFrame extends TraceReplayStackFrame { + private index: number = 0; + private handledInnerTxns: boolean = false; + private innerTxnGroupCount: number = 0; + private initialAppState: AppState | undefined; + private logicSigAddress: string | undefined; + + public state: ProgramState = { pc: 0, stack: [], scratch: new Map() }; + + constructor( + engine: TraceReplayEngine, + private readonly txnPath: number[], + private readonly programType: 'logic sig' | 'approval' | 'clear state', + private readonly programHash: Uint8Array, + private readonly programTrace: algosdk.modelsv2.SimulationOpcodeTraceUnit[], + private readonly trace: algosdk.modelsv2.SimulationTransactionExecTrace, + private readonly txnInfo: algosdk.modelsv2.PendingTransactionResponse, + ) { + super(engine); + this.state.pc = Number(programTrace[0].pc); + + const appID = this.currentAppID(); + if (typeof appID !== 'undefined') { + this.initialAppState = engine.currentAppState.get(appID)!.clone(); + } - private index: number = 0; - private handledInnerTxns: boolean = false; - private innerTxnGroupCount: number = 0; - private initialAppState: AppState | undefined; - - public state: ProgramState = { pc: 0, stack: [], scratch: new Map() }; - - constructor( - engine: TraceReplayEngine, - private readonly txnPath: number[], - private readonly programType: 'logic sig' | 'approval' | 'clear state', - private readonly programHash: Uint8Array, - private readonly programTrace: algosdk.modelsv2.SimulationOpcodeTraceUnit[], - private readonly trace: algosdk.modelsv2.SimulationTransactionExecTrace, - private readonly txnInfo: algosdk.modelsv2.PendingTransactionResponse + if ( + this.programType === 'logic sig' && + typeof this.txnInfo.txn.lsig !== 'undefined' ) { - super(engine); - this.state.pc = Number(programTrace[0].pc); - - const appID = this.currentAppID(); - if (typeof appID !== 'undefined') { - this.initialAppState = engine.currentAppState.get(appID)!.clone(); - } + let lsigBytes = this.txnInfo.txn.lsig.l; + if (typeof lsigBytes === 'string') { + lsigBytes = Buffer.from(lsigBytes, 'base64'); + } + const lsigAccount = new algosdk.LogicSigAccount(lsigBytes); + this.logicSigAddress = lsigAccount.address(); } + } - public currentAppID(): number | undefined { - if (typeof this.txnInfo.txn.txn.apid !== 'undefined') { - return Number(this.txnInfo.txn.txn.apid); - } - if (typeof this.txnInfo.applicationIndex !== 'undefined') { - return Number(this.txnInfo.applicationIndex); - } - return undefined; + public currentAppID(): number | undefined { + if (this.programType === 'logic sig') { + return undefined; } - - public name(): string { - const appID = this.currentAppID(); - if (appID) { - return `app ${appID} ${this.programType} program`; - } - return `${this.programType} program`; - } - - public sourceFile(): FrameSource { - const sourceInfo = this.engine.programHashToSource.get(this.programHash); - if (!sourceInfo) { - let name: string; - const appID = this.currentAppID(); - if (typeof appID !== 'undefined') { - name = `app ${appID} ${this.programType}.teal`; - } else { - name = `program ${Buffer.from(this.programHash).toString('base64url')}.teal`; - } - return { - name, - content: '// source not available', - }; - } - const location = sourceInfo.sourcemap.getLocationForPc(this.state.pc); - // If we can't find a location for this PC, just return the first source. - const sourceIndex = location ? location.sourceIndex : 0; - const source = sourceInfo.getFullSourcePath(sourceIndex); - return { - name: source, - path: source, - }; + if (typeof this.txnInfo.txn.txn.apid !== 'undefined') { + return Number(this.txnInfo.txn.txn.apid); + } + if (typeof this.txnInfo.applicationIndex !== 'undefined') { + return Number(this.txnInfo.applicationIndex); } + return undefined; + } - public sourceLocation(): FrameSourceLocation { - const sourceInfo = this.engine.programHashToSource.get(this.programHash); - if (!sourceInfo) { - return { line: 0 }; - } - const location = sourceInfo.sourcemap.getLocationForPc(this.state.pc); - if (!location) { - return { line: 0 }; - } - return { - line: location.line, - column: location.column, - }; + public name(): string { + const appID = this.currentAppID(); + if (typeof appID !== 'undefined') { + return `app ${appID} ${this.programType} program`; + } + if (typeof this.logicSigAddress !== 'undefined') { + return `logic sig ${this.logicSigAddress} program`; + } + return `${this.programType} program`; + } + + public sourceFile(): FrameSource { + const sourceInfo = this.engine.programHashToSource.get(this.programHash); + if (!sourceInfo) { + let name: string; + const appID = this.currentAppID(); + if (typeof appID !== 'undefined') { + name = `app ${appID} ${this.programType}.teal`; + } else if (typeof this.logicSigAddress !== 'undefined') { + name = `logic sig ${this.logicSigAddress}.teal`; + } else { + name = `program ${Buffer.from(this.programHash).toString( + 'base64url', + )}.teal`; + } + return { + name, + content: '// source not available', + }; + } + const location = sourceInfo.sourcemap.getLocationForPc(this.state.pc); + // If we can't find a location for this PC, just return the first source. + const sourceIndex = location ? location.sourceIndex : 0; + const source = sourceInfo.getFullSourcePath(sourceIndex); + return { + name: source, + path: source, + }; + } + + public sourceLocation(): FrameSourceLocation { + const sourceInfo = this.engine.programHashToSource.get(this.programHash); + if (!sourceInfo) { + return { line: 0 }; + } + const location = sourceInfo.sourcemap.getLocationForPc(this.state.pc); + if (!location) { + return { line: 0 }; + } + return { + line: location.line, + column: location.column, + }; + } + + public forward(stack: TraceReplayStackFrame[]): void { + const currentUnit = this.programTrace[this.index]; + this.processUnit(currentUnit); + + const spawnedInners = currentUnit.spawnedInners; + if (!this.handledInnerTxns && spawnedInners && spawnedInners.length !== 0) { + const innerGroupInfo: algosdk.modelsv2.PendingTransactionResponse[] = []; + const innerTraces: algosdk.modelsv2.SimulationTransactionExecTrace[] = []; + for (const innerIndex of spawnedInners) { + const innerTxnInfo = this.txnInfo.innerTxns![Number(innerIndex)]; + const innerTrace = this.trace.innerTrace![Number(innerIndex)]; + innerGroupInfo.push(innerTxnInfo); + innerTraces.push(innerTrace); + } + const expandedPath = this.txnPath.slice(); + expandedPath.push(this.innerTxnGroupCount); + const innerGroupFrame = new TransactionStackFrame( + this.engine, + expandedPath, + innerGroupInfo, + innerTraces, + ); + stack.push(innerGroupFrame); + this.handledInnerTxns = true; + this.innerTxnGroupCount++; + return; } - public forward(stack: TraceReplayStackFrame[]): void { - const currentUnit = this.programTrace[this.index]; - this.processUnit(currentUnit); + if (this.index + 1 < this.programTrace.length) { + this.index++; + this.state.pc = Number(this.programTrace[this.index].pc); + this.handledInnerTxns = false; + return; + } + stack.pop(); + } - const spawnedInners = currentUnit.spawnedInners; - if (!this.handledInnerTxns && spawnedInners && spawnedInners.length !== 0) { - const innerGroupInfo: algosdk.modelsv2.PendingTransactionResponse[] = []; - const innerTraces: algosdk.modelsv2.SimulationTransactionExecTrace[] = []; - for (const innerIndex of spawnedInners) { - const innerTxnInfo = this.txnInfo.innerTxns![Number(innerIndex)]; - const innerTrace = this.trace.innerTrace![Number(innerIndex)]; - innerGroupInfo.push(innerTxnInfo); - innerTraces.push(innerTrace); - } - const expandedPath = this.txnPath.slice(); - expandedPath.push(this.innerTxnGroupCount); - const innerGroupFrame = new TransactionStackFrame(this.engine, expandedPath, innerGroupInfo, innerTraces); - stack.push(innerGroupFrame); - this.handledInnerTxns = true; - this.innerTxnGroupCount++; - return; - } + private processUnit(unit: algosdk.modelsv2.SimulationOpcodeTraceUnit) { + this.state.pc = Number(unit.pc); - if (this.index + 1 < this.programTrace.length) { - this.index++; - this.state.pc = Number(this.programTrace[this.index].pc); - this.handledInnerTxns = false; - return; - } - stack.pop(); + const stackPopCount = unit.stackPopCount ? Number(unit.stackPopCount) : 0; + if (stackPopCount > this.state.stack.length) { + throw new Error( + `Stack underflow at pc ${unit.pc}: ${stackPopCount} > ${this.state.stack.length}`, + ); + } + this.state.stack = this.state.stack.slice( + 0, + this.state.stack.length - stackPopCount, + ); + if (unit.stackAdditions) { + this.state.stack.push(...unit.stackAdditions); } - private processUnit(unit: algosdk.modelsv2.SimulationOpcodeTraceUnit) { - this.state.pc = Number(unit.pc); - - const stackPopCount = unit.stackPopCount ? Number(unit.stackPopCount) : 0; - if (stackPopCount > this.state.stack.length) { - throw new Error(`Stack underflow at pc ${unit.pc}: ${stackPopCount} > ${this.state.stack.length}`); - } - this.state.stack = this.state.stack.slice(0, this.state.stack.length - stackPopCount); - if (unit.stackAdditions) { - this.state.stack.push(...unit.stackAdditions); - } - - for (const scratchWrite of unit.scratchChanges || []) { - const slot = Number(scratchWrite.slot); - if (slot < 0 || slot >= 256) { - throw new Error(`Invalid scratch slot ${slot}`); - } - const newValue = scratchWrite.newValue; - if (Number(newValue.type) === 2 && !newValue.uint) { - // When setting to 0, delete the entry, since 0 is the default. - this.state.scratch.delete(slot); - } else { - this.state.scratch.set(slot, newValue); - } - } + for (const scratchWrite of unit.scratchChanges || []) { + const slot = Number(scratchWrite.slot); + if (slot < 0 || slot >= 256) { + throw new Error(`Invalid scratch slot ${slot}`); + } + const newValue = scratchWrite.newValue; + if (Number(newValue.type) === 2 && !newValue.uint) { + // When setting to 0, delete the entry, since 0 is the default. + this.state.scratch.delete(slot); + } else { + this.state.scratch.set(slot, newValue); + } + } - if (unit.stateChanges && unit.stateChanges.length !== 0) { - const appID = this.currentAppID(); - if (typeof appID === 'undefined') { - throw new Error('No appID'); + if (unit.stateChanges && unit.stateChanges.length !== 0) { + const appID = this.currentAppID(); + if (typeof appID === 'undefined') { + throw new Error('No appID'); + } + + const state = this.engine.currentAppState.get(appID); + if (!state) { + throw new Error(`No state for appID ${appID}`); + } + + for (const stateChange of unit.stateChanges) { + switch (stateChange.appStateType) { + case 'g': + if (stateChange.operation === 'w') { + state.globalState.set(stateChange.key, stateChange.newValue!); + } else if (stateChange.operation === 'd') { + state.globalState.delete(stateChange.key); } - - const state = this.engine.currentAppState.get(appID); - if (!state) { - throw new Error(`No state for appID ${appID}`); + break; + case 'l': + if (stateChange.operation === 'w') { + const accountState = state.localState.get(stateChange.account!); + if (!accountState) { + const newState = new ByteArrayMap(); + newState.set(stateChange.key, stateChange.newValue!); + state.localState.set(stateChange.account!, newState); + } else { + accountState.set(stateChange.key, stateChange.newValue!); + } + } else if (stateChange.operation === 'd') { + const accountState = state.localState.get(stateChange.account!); + if (accountState) { + accountState.delete(stateChange.key); + } } - - for (const stateChange of unit.stateChanges) { - switch (stateChange.appStateType) { - case 'g': - if (stateChange.operation === 'w') { - state.globalState.set(stateChange.key, stateChange.newValue!); - } else if (stateChange.operation === 'd') { - state.globalState.delete(stateChange.key); - } - break; - case 'l': - if (stateChange.operation === 'w') { - const accountState = state.localState.get(stateChange.account!); - if (!accountState) { - const newState = new ByteArrayMap(); - newState.set(stateChange.key, stateChange.newValue!); - state.localState.set(stateChange.account!, newState); - } else { - accountState.set(stateChange.key, stateChange.newValue!); - } - } else if (stateChange.operation === 'd') { - const accountState = state.localState.get(stateChange.account!); - if (accountState) { - accountState.delete(stateChange.key); - } - } - break; - case 'b': - if (stateChange.operation === 'w') { - state.boxState.set(stateChange.key, stateChange.newValue!); - } else if (stateChange.operation === 'd') { - state.boxState.delete(stateChange.key); - } - } + break; + case 'b': + if (stateChange.operation === 'w') { + state.boxState.set(stateChange.key, stateChange.newValue!); + } else if (stateChange.operation === 'd') { + state.boxState.delete(stateChange.key); } } + } } + } - public backward(stack: TraceReplayStackFrame[]): void { - if (this.handledInnerTxns) { - // We can roll this back without any other effects - this.handledInnerTxns = false; - return; - } - if (this.index === 0) { - stack.pop(); - return; - } - const targetIndex = this.index - 1; - this.reset(); - while (this.index < targetIndex) { - this.forward(stack); - } + public backward(stack: TraceReplayStackFrame[]): void { + if (this.handledInnerTxns) { + // We can roll this back without any other effects + this.handledInnerTxns = false; + return; } - - private reset() { - this.index = 0; - this.handledInnerTxns = false; - this.innerTxnGroupCount = 0; - this.state.pc = Number(this.programTrace[0].pc); - this.state.stack = []; - this.state.scratch.clear(); - if (typeof this.initialAppState !== 'undefined') { - this.engine.currentAppState.set(this.currentAppID()!, this.initialAppState); - } + if (this.index === 0) { + stack.pop(); + return; + } + const targetIndex = this.index - 1; + this.reset(); + while (this.index < targetIndex) { + this.engine.forward(); + } + } + + private reset() { + this.index = 0; + this.handledInnerTxns = false; + this.innerTxnGroupCount = 0; + this.state.pc = Number(this.programTrace[0].pc); + this.state.stack = []; + this.state.scratch.clear(); + if (typeof this.initialAppState !== 'undefined') { + this.engine.currentAppState.set( + this.currentAppID()!, + this.initialAppState, + ); } + } } diff --git a/src/debugAdapter/txnGroupWalkerRuntime.ts b/src/debugAdapter/txnGroupWalkerRuntime.ts index dbc62b5..970f5d3 100644 --- a/src/debugAdapter/txnGroupWalkerRuntime.ts +++ b/src/debugAdapter/txnGroupWalkerRuntime.ts @@ -1,356 +1,403 @@ import { EventEmitter } from 'events'; import { RuntimeEvents } from './debugRequestHandlers'; import { AppState } from './appState'; -import { FrameSourceLocation, TraceReplayEngine, TraceReplayStackFrame } from './traceReplayEngine'; -import { FileAccessor, TEALDebuggingAssets, TxnGroupSourceDescriptor } from './utils'; +import { + FrameSourceLocation, + TraceReplayEngine, + TraceReplayStackFrame, +} from './traceReplayEngine'; +import { + FileAccessor, + TEALDebuggingAssets, + TxnGroupSourceDescriptor, +} from './utils'; export interface IRuntimeBreakpoint { - id: number; - verified: boolean; - location: IRuntimeBreakpointLocation; + id: number; + verified: boolean; + location: IRuntimeBreakpointLocation; } export interface IRuntimeBreakpointLocation { - line: number; - column?: number; + line: number; + column?: number; } interface IRuntimeStepInTargets { - id: number; - label: string; + id: number; + label: string; } interface IRuntimeStack { - count: number; - frames: TraceReplayStackFrame[]; + count: number; + frames: TraceReplayStackFrame[]; } export class TxnGroupWalkerRuntime extends EventEmitter { - // maps from sourceFile to array of IRuntimeBreakpoint - private breakPoints = new Map(); - - private engine: TraceReplayEngine; - - // since we want to send breakpoint events, we will assign an id to every event - // so that the frontend can match events with breakpoints. - private breakpointId = 1; - - constructor(private fileAccessor: FileAccessor, debugAssets: TEALDebuggingAssets) { - super(); - this.engine = new TraceReplayEngine(debugAssets); - } - - private nextTickWithErrorReporting(fn: () => Promise | void) { - setTimeout(async () => { - try { - await fn(); - } catch (e) { - console.error(e); - this.sendEvent(RuntimeEvents.error, e); - } - }, 0); - } - - /** - * Start executing the given program. - */ - public start(stopOnEntry: boolean, debug: boolean) { - this.nextTickWithErrorReporting(() => { - if (debug) { - for (let [_, sourceDescriptor] of this.engine.programHashToSource.entriesHex()) { - if (!sourceDescriptor) { - continue; - } - for (const sourcePath of sourceDescriptor.sourcePaths()) { - this.verifyBreakpoints(sourcePath, false); - } - } - - if (stopOnEntry) { - this.sendEvent(RuntimeEvents.stopOnEntry); - } else { - // we just start to run until we hit a breakpoint, an exception, or the end of the program - this.continue(false); - } - } else { - this.continue(false); - } - }); - } - - private updateCurrentLine(reverse: boolean): boolean { - if (reverse) { - if (!this.engine.backward()) { - this.sendEvent(RuntimeEvents.stopOnEntry); - return false; - } - } else { - if (!this.engine.forward()) { - this.sendEvent(RuntimeEvents.end); - return false; - } - } - - return true; - } - - /** - * Continue execution to the end/beginning. - */ - public continue(reverse: boolean) { - this.nextTickWithErrorReporting(() => { - while (true) { - if (!this.updateCurrentLine(reverse)) { - break; - } - if (this.hitBreakpoint()) { - break; - } - } - }); - } - - /** - * "Step into" - */ - public stepIn(targetId: number | undefined) { - this.nextTickWithErrorReporting(() => { - if (this.updateCurrentLine(false)) { - if (!this.hitBreakpoint()) { - this.sendEvent(RuntimeEvents.stopOnStep); - } - } - }); - } - - /** - * "Step out" - */ - public stepOut() { - const targetStackDepth = this.engine.stack.length - 1; - if (targetStackDepth <= 0) { - this.continue(false); - } - this.nextTickWithErrorReporting(() => { - while (targetStackDepth < this.engine.stack.length) { - if (!this.updateCurrentLine(false)) { - return; - } - if (this.hitBreakpoint()) { - return; - } - } - this.sendEvent(RuntimeEvents.stopOnStep); - }); - } - - /** - * "Step over" - */ - public step(reverse: boolean) { - const targetStackDepth = this.engine.stack.length; - this.nextTickWithErrorReporting(() => { - do { - if (!this.updateCurrentLine(reverse)) { - return; - } - if (this.hitBreakpoint()) { - return; - } - } while (targetStackDepth < this.engine.stack.length); - this.sendEvent(RuntimeEvents.stopOnStep); - }); - } - - public getStepInTargets(frameId: number): IRuntimeStepInTargets[] { - return []; - } - - public stackLength(): number { - return this.engine.stack.length; - } - - /** - * Returns a 'stacktrace' where every frame is a TraceReplayStackFrame. - */ - public stack(startFrame: number, endFrame: number): IRuntimeStack { - if (this.engine.stack.length < endFrame) { - endFrame = this.engine.stack.length; - } - const frames: TraceReplayStackFrame[] = []; - for (let i = startFrame; i < endFrame; i++) { - frames.push(this.engine.stack[i]); - } - return { - frames: frames, - count: this.engine.stack.length - }; - } - - public getStackFrame(index: number): TraceReplayStackFrame | undefined { - if (index < 0 || index >= this.engine.stack.length) { - return undefined; - } - return this.engine.stack[index]; - } - - /* - * Return all possible breakpoint locations for the file with given path. - */ - public breakpointLocations(filePath: string): IRuntimeBreakpointLocation[] { - const locations: IRuntimeBreakpointLocation[] = []; - - const sourceDescriptors = this.findSourceDescriptorsForPath(filePath); - for (const { descriptor, sourceIndex } of sourceDescriptors) { - for (const pc of descriptor.sourcemap.getPcs()) { - const location = descriptor.sourcemap.getLocationForPc(pc)!; - if (location.sourceIndex === sourceIndex) { - locations.push({ - line: location.line, - column: location.column - }); - } - } - } - - return locations; - } - - /* - * Set breakpoint in file with given line. - */ - public setBreakPoint(path: string, line: number, column?: number): IRuntimeBreakpoint { - path = this.normalizePathAndCasing(path); - - const bp: IRuntimeBreakpoint = { verified: false, location: { line, column }, id: this.breakpointId++ }; - let bps = this.breakPoints.get(path); - if (!bps) { - bps = []; - this.breakPoints.set(path, bps); - } - bps.push(bp); - - this.verifyBreakpoints(path, true); - - return bp; - } - - public clearBreakpoints(path: string): void { - this.breakPoints.delete(this.normalizePathAndCasing(path)); - } - - public getAppStateReferences(): number[] { - const apps = Array.from(this.engine.initialAppState.keys()); - return apps.sort((a, b) => a - b); - } - - public getAppLocalStateAccounts(appID: number): string[] { - const app = this.engine.initialAppState.get(appID); - if (!app) { - return []; - } - const accounts = Array.from(app.localState.keys()); - return accounts.sort(); - } - - public getAppState(appID: number): AppState { - return this.engine.currentAppState.get(appID) || new AppState(); - } - - private hitBreakpoint(): boolean { - const frame = this.engine.currentFrame(); - const sourceInfo = frame.sourceFile(); - const sourceLocation = frame.sourceLocation(); - if (sourceInfo.path) { - const breakpoints = this.breakPoints.get(sourceInfo.path) || []; - const bps = breakpoints.filter(bp => this.isFrameLocationOnBreakpoint(sourceLocation, bp.location)); - if (bps.length !== 0) { - // send 'stopped' event - this.sendEvent(RuntimeEvents.stopOnBreakpoint); - - // the following shows the use of 'breakpoint' events to update properties of a breakpoint in the UI - // if breakpoint is not yet verified, verify it now and send a 'breakpoint' update event - if (!bps[0].verified) { - bps[0].verified = true; - this.sendEvent(RuntimeEvents.breakpointValidated, bps[0]); - } - - return true; - } - } - return false; - } - - // Helper functions - - private verifyBreakpoints(path: string, silent: boolean) { - const bps = this.breakPoints.get(path); - if (typeof bps === 'undefined') { - return; - } - - const sourceDescriptors = this.findSourceDescriptorsForPath(path); - for (const bp of bps) { - if (bp.verified) { - continue; - } - const location = bp.location; - for (const { descriptor, sourceIndex } of sourceDescriptors) { - const pcs = descriptor.sourcemap.getPcsOnSourceLine(sourceIndex, location.line); - if (typeof location.column === 'undefined' && pcs.length !== 0) { - const sortedPcs = pcs.slice().sort((a, b) => a.column - b.column); - location.column = sortedPcs[0].column; - } - - if (typeof location.column !== 'undefined' && pcs.some(({ column }) => column === location.column)) { - bp.verified = true; - if (!silent) { - this.sendEvent(RuntimeEvents.breakpointValidated, bp); - } - } - } - } - } - - private sendEvent(event: string, ...args: any[]): void { - setTimeout(() => { - this.emit(event, ...args); - }, 0); - } - - private normalizePathAndCasing(filePath: string) { - if (this.fileAccessor.isWindows) { - return filePath.replace(/\//g, '\\').toLowerCase(); - } else { - return filePath.replace(/\\/g, '/'); - } - } - - private isFrameLocationOnBreakpoint(location: FrameSourceLocation, bp: IRuntimeBreakpointLocation): boolean { - if (location.line !== bp.line) { - return false; - } - if (typeof bp.column === 'undefined' || typeof location.column === 'undefined') { - return true; - } - return bp.column === location.column; - } - - private findSourceDescriptorsForPath(filePath: string): Array<{ descriptor: TxnGroupSourceDescriptor, sourceIndex: number }> { - filePath = this.normalizePathAndCasing(filePath); - - const sourceDescriptors: Array<{ descriptor: TxnGroupSourceDescriptor, sourceIndex: number }> = []; - - for (const [_, entrySourceInfo] of this.engine.programHashToSource.entriesHex()) { - if (!entrySourceInfo) { - continue; - } - const entrySourceIndex = entrySourceInfo.sourcePaths().indexOf(filePath); - if (entrySourceIndex !== -1) { - sourceDescriptors.push({ descriptor: entrySourceInfo, sourceIndex: entrySourceIndex }); - } - } - - return sourceDescriptors; - } + // maps from sourceFile to array of IRuntimeBreakpoint + private breakPoints = new Map(); + + private engine: TraceReplayEngine; + + // since we want to send breakpoint events, we will assign an id to every event + // so that the frontend can match events with breakpoints. + private breakpointId = 1; + + constructor( + private fileAccessor: FileAccessor, + debugAssets: TEALDebuggingAssets, + ) { + super(); + this.engine = new TraceReplayEngine(debugAssets); + } + + private nextTickWithErrorReporting(fn: () => Promise | void) { + setTimeout(async () => { + try { + await fn(); + } catch (e) { + console.error(e); + this.sendEvent(RuntimeEvents.error, e); + } + }, 0); + } + + /** + * Start executing the given program. + */ + public start(stopOnEntry: boolean, debug: boolean) { + this.nextTickWithErrorReporting(() => { + if (debug) { + for (const [ + _, + sourceDescriptor, + ] of this.engine.programHashToSource.entriesHex()) { + if (!sourceDescriptor) { + continue; + } + for (const sourcePath of sourceDescriptor.sourcePaths()) { + this.verifyBreakpoints(sourcePath, false); + } + } + + if (stopOnEntry) { + this.sendEvent(RuntimeEvents.stopOnEntry); + } else { + // we just start to run until we hit a breakpoint, an exception, or the end of the program + this.continue(false); + } + } else { + this.continue(false); + } + }); + } + + private updateCurrentLine(reverse: boolean): boolean { + if (reverse) { + if (!this.engine.backward()) { + this.sendEvent(RuntimeEvents.stopOnEntry); + return false; + } + } else { + if (!this.engine.forward()) { + this.sendEvent(RuntimeEvents.end); + return false; + } + } + + return true; + } + + /** + * Continue execution to the end/beginning. + */ + public continue(reverse: boolean) { + this.nextTickWithErrorReporting(() => { + for (;;) { + if (!this.updateCurrentLine(reverse)) { + break; + } + if (this.hitBreakpoint()) { + break; + } + } + }); + } + + /** + * "Step into" + */ + public stepIn(targetId: number | undefined) { + this.nextTickWithErrorReporting(() => { + if (this.updateCurrentLine(false)) { + if (!this.hitBreakpoint()) { + this.sendEvent(RuntimeEvents.stopOnStep); + } + } + }); + } + + /** + * "Step out" + */ + public stepOut() { + const targetStackDepth = this.engine.stack.length - 1; + if (targetStackDepth <= 0) { + this.continue(false); + } + this.nextTickWithErrorReporting(() => { + while (targetStackDepth < this.engine.stack.length) { + if (!this.updateCurrentLine(false)) { + return; + } + if (this.hitBreakpoint()) { + return; + } + } + this.sendEvent(RuntimeEvents.stopOnStep); + }); + } + + /** + * "Step over" + */ + public step(reverse: boolean) { + const targetStackDepth = this.engine.stack.length; + this.nextTickWithErrorReporting(() => { + do { + if (!this.updateCurrentLine(reverse)) { + return; + } + if (this.hitBreakpoint()) { + return; + } + } while (targetStackDepth < this.engine.stack.length); + this.sendEvent(RuntimeEvents.stopOnStep); + }); + } + + public getStepInTargets(frameId: number): IRuntimeStepInTargets[] { + return []; + } + + public stackLength(): number { + return this.engine.stack.length; + } + + /** + * Returns a 'stacktrace' where every frame is a TraceReplayStackFrame. + */ + public stack(startFrame: number, endFrame: number): IRuntimeStack { + if (this.engine.stack.length < endFrame) { + endFrame = this.engine.stack.length; + } + const frames: TraceReplayStackFrame[] = []; + for (let i = startFrame; i < endFrame; i++) { + frames.push(this.engine.stack[i]); + } + return { + frames: frames, + count: this.engine.stack.length, + }; + } + + public getStackFrame(index: number): TraceReplayStackFrame | undefined { + if (index < 0 || index >= this.engine.stack.length) { + return undefined; + } + return this.engine.stack[index]; + } + + /* + * Return all possible breakpoint locations for the file with given path. + */ + public breakpointLocations(filePath: string): IRuntimeBreakpointLocation[] { + const locations: IRuntimeBreakpointLocation[] = []; + + const sourceDescriptors = this.findSourceDescriptorsForPath(filePath); + for (const { descriptor, sourceIndex } of sourceDescriptors) { + for (const pc of descriptor.sourcemap.getPcs()) { + const location = descriptor.sourcemap.getLocationForPc(pc)!; + if (location.sourceIndex === sourceIndex) { + locations.push({ + line: location.line, + column: location.column, + }); + } + } + } + + return locations; + } + + /* + * Set breakpoint in file with given line. + */ + public setBreakPoint( + path: string, + line: number, + column?: number, + ): IRuntimeBreakpoint { + path = this.normalizePathAndCasing(path); + + const bp: IRuntimeBreakpoint = { + verified: false, + location: { line, column }, + id: this.breakpointId++, + }; + let bps = this.breakPoints.get(path); + if (!bps) { + bps = []; + this.breakPoints.set(path, bps); + } + bps.push(bp); + + this.verifyBreakpoints(path, true); + + return bp; + } + + public clearBreakpoints(path: string): void { + this.breakPoints.delete(this.normalizePathAndCasing(path)); + } + + public getAppStateReferences(): number[] { + const apps = Array.from(this.engine.initialAppState.keys()); + return apps.sort((a, b) => a - b); + } + + public getAppLocalStateAccounts(appID: number): string[] { + const app = this.engine.initialAppState.get(appID); + if (!app) { + return []; + } + const accounts = Array.from(app.localState.keys()); + return accounts.sort(); + } + + public getAppState(appID: number): AppState { + return this.engine.currentAppState.get(appID) || new AppState(); + } + + private hitBreakpoint(): boolean { + const frame = this.engine.currentFrame(); + const sourceInfo = frame.sourceFile(); + const sourceLocation = frame.sourceLocation(); + if (sourceInfo.path) { + const breakpoints = this.breakPoints.get(sourceInfo.path) || []; + const bps = breakpoints.filter((bp) => + this.isFrameLocationOnBreakpoint(sourceLocation, bp.location), + ); + if (bps.length !== 0) { + // send 'stopped' event + this.sendEvent(RuntimeEvents.stopOnBreakpoint); + + // the following shows the use of 'breakpoint' events to update properties of a breakpoint in the UI + // if breakpoint is not yet verified, verify it now and send a 'breakpoint' update event + if (!bps[0].verified) { + bps[0].verified = true; + this.sendEvent(RuntimeEvents.breakpointValidated, bps[0]); + } + + return true; + } + } + return false; + } + + // Helper functions + + private verifyBreakpoints(path: string, silent: boolean) { + const bps = this.breakPoints.get(path); + if (typeof bps === 'undefined') { + return; + } + + const sourceDescriptors = this.findSourceDescriptorsForPath(path); + for (const bp of bps) { + if (bp.verified) { + continue; + } + const location = bp.location; + for (const { descriptor, sourceIndex } of sourceDescriptors) { + const pcs = descriptor.sourcemap.getPcsOnSourceLine( + sourceIndex, + location.line, + ); + if (typeof location.column === 'undefined' && pcs.length !== 0) { + const sortedPcs = pcs.slice().sort((a, b) => a.column - b.column); + location.column = sortedPcs[0].column; + } + + if ( + typeof location.column !== 'undefined' && + pcs.some(({ column }) => column === location.column) + ) { + bp.verified = true; + if (!silent) { + this.sendEvent(RuntimeEvents.breakpointValidated, bp); + } + } + } + } + } + + private sendEvent(event: string, ...args: any[]): void { + setTimeout(() => { + this.emit(event, ...args); + }, 0); + } + + private normalizePathAndCasing(filePath: string) { + if (this.fileAccessor.isWindows) { + return filePath.replace(/\//g, '\\').toLowerCase(); + } else { + return filePath.replace(/\\/g, '/'); + } + } + + private isFrameLocationOnBreakpoint( + location: FrameSourceLocation, + bp: IRuntimeBreakpointLocation, + ): boolean { + if (location.line !== bp.line) { + return false; + } + if ( + typeof bp.column === 'undefined' || + typeof location.column === 'undefined' + ) { + return true; + } + return bp.column === location.column; + } + + private findSourceDescriptorsForPath( + filePath: string, + ): Array<{ descriptor: TxnGroupSourceDescriptor; sourceIndex: number }> { + filePath = this.normalizePathAndCasing(filePath); + + const sourceDescriptors: Array<{ + descriptor: TxnGroupSourceDescriptor; + sourceIndex: number; + }> = []; + + for (const [ + _, + entrySourceInfo, + ] of this.engine.programHashToSource.entriesHex()) { + if (!entrySourceInfo) { + continue; + } + const entrySourceIndex = entrySourceInfo.sourcePaths().indexOf(filePath); + if (entrySourceIndex !== -1) { + sourceDescriptors.push({ + descriptor: entrySourceInfo, + sourceIndex: entrySourceIndex, + }); + } + } + + return sourceDescriptors; + } } diff --git a/src/debugAdapter/utils.ts b/src/debugAdapter/utils.ts index bcf08e6..c8fc837 100644 --- a/src/debugAdapter/utils.ts +++ b/src/debugAdapter/utils.ts @@ -3,235 +3,285 @@ import * as JSONbigWithoutConfig from 'json-bigint'; import * as algosdk from 'algosdk'; export interface FileAccessor { - isWindows: boolean; - readFile(path: string): Promise; - writeFile(path: string, contents: Uint8Array): Promise; + isWindows: boolean; + readFile(path: string): Promise; + writeFile(path: string, contents: Uint8Array): Promise; } export function isAsciiPrintable(data: Uint8Array): boolean { - for (let i = 0; i < data.length; i++) { - if (data[i] < 32 || data[i] > 126) { - return false; - } + for (let i = 0; i < data.length; i++) { + if (data[i] < 32 || data[i] > 126) { + return false; } - return true; + } + return true; } -export function limitArray(array: Array, start?: number, count?: number): Array { - if (start === undefined) { - start = 0; - } - if (count === undefined) { - count = array.length; - } - if (start < 0) { - start = 0; - } - if (count < 0) { - count = 0; - } - if (start >= array.length) { - return []; - } - if (start + count > array.length) { - count = array.length - start; - } - return array.slice(start, start + count); +export function limitArray( + array: Array, + start?: number, + count?: number, +): Array { + if (start === undefined) { + start = 0; + } + if (count === undefined) { + count = array.length; + } + if (start < 0) { + start = 0; + } + if (count < 0) { + count = 0; + } + if (start >= array.length) { + return []; + } + if (start + count > array.length) { + count = array.length - start; + } + return array.slice(start, start + count); } // TODO: replace with algosdk.parseJson once it is available in v3 export function parseJsonWithBigints(json: string): any { - // Our tests wants this lib to be imported as `import * as JSONbig from 'json-bigint';`, - // but running this in vscode wants it to be imported as `import JSONbig from 'json-bigint';`. - // This is a hack to allow both. - let target = JSONbigWithoutConfig; - if (target.default) { - target = target.default; - } - const JSON_BIG = target({ useNativeBigInt: true, strict: true }); - return JSON_BIG.parse(json); + // Our tests wants this lib to be imported as `import * as JSONbig from 'json-bigint';`, + // but running this in vscode wants it to be imported as `import JSONbig from 'json-bigint';`. + // This is a hack to allow both. + let target = JSONbigWithoutConfig; + if (target.default) { + target = target.default; + } + const JSON_BIG = target({ useNativeBigInt: true, strict: true }); + return JSON_BIG.parse(json); } export class ByteArrayMap { - - private map: Map; - - constructor(entries?: Iterable<[Uint8Array, T]> | null) { - this.map = new Map(); - for (const [key, value] of entries || []) { - this.set(key, value); - } - } + private map: Map; - public get size(): number { - return this.map.size; + constructor(entries?: Iterable<[Uint8Array, T]> | null) { + this.map = new Map(); + for (const [key, value] of entries || []) { + this.set(key, value); } + } - public set(key: Uint8Array, value: T): void { - this.map.set(Buffer.from(key).toString('hex'), value); - } + public get size(): number { + return this.map.size; + } - public setHex(key: string, value: T): void { - this.map.set(key, value); - } + public set(key: Uint8Array, value: T): void { + this.map.set(Buffer.from(key).toString('hex'), value); + } - public get(key: Uint8Array): T | undefined { - return this.map.get(Buffer.from(key).toString('hex')); - } + public setHex(key: string, value: T): void { + this.map.set(key, value); + } - public getHex(key: string): T | undefined { - return this.map.get(key); - } + public get(key: Uint8Array): T | undefined { + return this.map.get(Buffer.from(key).toString('hex')); + } - public hasHex(key: string): boolean { - return this.map.has(key); - } + public getHex(key: string): T | undefined { + return this.map.get(key); + } - public has(key: Uint8Array): boolean { - return this.map.has(Buffer.from(key).toString('hex')); - } + public hasHex(key: string): boolean { + return this.map.has(key); + } - public delete(key: Uint8Array): boolean { - return this.map.delete(Buffer.from(key).toString('hex')); - } + public has(key: Uint8Array): boolean { + return this.map.has(Buffer.from(key).toString('hex')); + } - public deleteHex(key: string): boolean { - return this.map.delete(key); - } + public delete(key: Uint8Array): boolean { + return this.map.delete(Buffer.from(key).toString('hex')); + } - public clear(): void { - this.map.clear(); - } + public deleteHex(key: string): boolean { + return this.map.delete(key); + } - public* entries(): IterableIterator<[Uint8Array, T]> { - for (const [key, value] of this.map.entries()) { - yield [Buffer.from(key, 'hex'), value]; - } - } + public clear(): void { + this.map.clear(); + } - public entriesHex(): IterableIterator<[string, T]> { - return this.map.entries(); + public *entries(): IterableIterator<[Uint8Array, T]> { + for (const [key, value] of this.map.entries()) { + yield [Buffer.from(key, 'hex'), value]; } + } - public clone(): ByteArrayMap { - const clone = new ByteArrayMap(); - clone.map = new Map(this.map.entries()); - return clone; - } + public entriesHex(): IterableIterator<[string, T]> { + return this.map.entries(); + } + + public clone(): ByteArrayMap { + const clone = new ByteArrayMap(); + clone.map = new Map(this.map.entries()); + return clone; + } } function filePathRelativeTo(base: string, filePath: string): string { - if (path.isAbsolute(filePath)) { - return filePath; - } - return path.join(path.dirname(base), filePath); + if (path.isAbsolute(filePath)) { + return filePath; + } + return path.join(path.dirname(base), filePath); } export class TxnGroupSourceDescriptor { - public readonly sourcemapFileLocation: string; - public readonly sourcemap: algosdk.SourceMap; - public readonly hash: string; - - constructor({ - sourcemapFileLocation, - sourcemap, - hash, - }: { - sourcemapFileLocation: string, - sourcemap: algosdk.SourceMap, - hash: string, - }) { - this.sourcemapFileLocation = sourcemapFileLocation; - this.sourcemap = sourcemap; - this.hash = hash; - } - - public sourcePaths(): string[] { - return this.sourcemap.sources.map((_, index) => this.getFullSourcePath(index)); - } - - public getFullSourcePath(index: number): string { - return filePathRelativeTo(this.sourcemapFileLocation, this.sourcemap.sources[index]); - } - - static async fromJSONObj(fileAccessor: FileAccessor, originFile: string, data: Record): Promise { - const sourcemapFileLocation = filePathRelativeTo(originFile, data['sourcemap-location']); - const rawSourcemap = Buffer.from(await fileAccessor.readFile(sourcemapFileLocation)); - const sourcemap = new algosdk.SourceMap(JSON.parse(rawSourcemap.toString('utf-8'))); - - return new TxnGroupSourceDescriptor({ - sourcemapFileLocation, - sourcemap, - hash: data['hash'], - }); - } + public readonly sourcemapFileLocation: string; + public readonly sourcemap: algosdk.SourceMap; + public readonly hash: string; + + constructor({ + sourcemapFileLocation, + sourcemap, + hash, + }: { + sourcemapFileLocation: string; + sourcemap: algosdk.SourceMap; + hash: string; + }) { + this.sourcemapFileLocation = sourcemapFileLocation; + this.sourcemap = sourcemap; + this.hash = hash; + } + + public sourcePaths(): string[] { + return this.sourcemap.sources.map((_, index) => + this.getFullSourcePath(index), + ); + } + + public getFullSourcePath(index: number): string { + return filePathRelativeTo( + this.sourcemapFileLocation, + this.sourcemap.sources[index], + ); + } + + static async fromJSONObj( + fileAccessor: FileAccessor, + originFile: string, + data: Record, + ): Promise { + const sourcemapFileLocation = filePathRelativeTo( + originFile, + data['sourcemap-location'], + ); + const rawSourcemap = Buffer.from( + await fileAccessor.readFile(sourcemapFileLocation), + ); + const sourcemap = new algosdk.SourceMap( + JSON.parse(rawSourcemap.toString('utf-8')), + ); + + return new TxnGroupSourceDescriptor({ + sourcemapFileLocation, + sourcemap, + hash: data['hash'], + }); + } } export class TxnGroupSourceDescriptorList { - private _txnGroupSources: Array; - - constructor({ txnGroupSources }: { - txnGroupSources: Array; - }) { - this._txnGroupSources = txnGroupSources; - } - - public get txnGroupSources(): Array { - return this._txnGroupSources; - } - - public findByHash(hash: string | Uint8Array): TxnGroupSourceDescriptor | undefined { - if (typeof hash !== 'string') { - hash = Buffer.from(hash).toString('base64'); - } - for (let i = 0; i < this._txnGroupSources.length; i++) { - if (this._txnGroupSources[i].hash && this._txnGroupSources[i].hash === hash) { - return this._txnGroupSources[i]; - } - } - return undefined; - } - - static async loadFromFile(fileAccessor: FileAccessor, txnGroupSourcesDescriptionPath: string): Promise { - const rawGroupSourcesDescription = Buffer.from(await fileAccessor.readFile(txnGroupSourcesDescriptionPath)); - const jsonGroupSourcesDescription = JSON.parse(rawGroupSourcesDescription.toString('utf-8')) as Record; - const txnGroupSources = (jsonGroupSourcesDescription['txn-group-sources'] as any[]).map(source => TxnGroupSourceDescriptor.fromJSONObj(fileAccessor, txnGroupSourcesDescriptionPath, source)); - return new TxnGroupSourceDescriptorList({ - txnGroupSources: await Promise.all(txnGroupSources) - }); - } + private _txnGroupSources: Array; + + constructor({ + txnGroupSources, + }: { + txnGroupSources: Array; + }) { + this._txnGroupSources = txnGroupSources; + } + + public get txnGroupSources(): Array { + return this._txnGroupSources; + } + + public findByHash( + hash: string | Uint8Array, + ): TxnGroupSourceDescriptor | undefined { + if (typeof hash !== 'string') { + hash = Buffer.from(hash).toString('base64'); + } + for (let i = 0; i < this._txnGroupSources.length; i++) { + if ( + this._txnGroupSources[i].hash && + this._txnGroupSources[i].hash === hash + ) { + return this._txnGroupSources[i]; + } + } + return undefined; + } + + static async loadFromFile( + fileAccessor: FileAccessor, + txnGroupSourcesDescriptionPath: string, + ): Promise { + const rawGroupSourcesDescription = Buffer.from( + await fileAccessor.readFile(txnGroupSourcesDescriptionPath), + ); + const jsonGroupSourcesDescription = JSON.parse( + rawGroupSourcesDescription.toString('utf-8'), + ) as Record; + const txnGroupSources = ( + jsonGroupSourcesDescription['txn-group-sources'] as any[] + ).map((source) => + TxnGroupSourceDescriptor.fromJSONObj( + fileAccessor, + txnGroupSourcesDescriptionPath, + source, + ), + ); + return new TxnGroupSourceDescriptorList({ + txnGroupSources: await Promise.all(txnGroupSources), + }); + } } - - export class TEALDebuggingAssets { - private _simulateResponse: algosdk.modelsv2.SimulateResponse; - private _txnGroupDescriptorList: TxnGroupSourceDescriptorList; - - constructor(simulateResponse: algosdk.modelsv2.SimulateResponse, txnGroupDescriptorList: TxnGroupSourceDescriptorList) { - this._simulateResponse = simulateResponse; - this._txnGroupDescriptorList = txnGroupDescriptorList; - } - - public get simulateResponse(): algosdk.modelsv2.SimulateResponse { - return this._simulateResponse; - } - - public get txnGroupDescriptorList(): TxnGroupSourceDescriptorList { - return this._txnGroupDescriptorList; - } - - static async loadFromFiles(fileAccessor: FileAccessor, simulateResponsePath: string, txnGroupSourcesDescriptionPath: string): Promise { - const rawSimulateResponse = Buffer.from(await fileAccessor.readFile(simulateResponsePath)); - const simulateResponse = algosdk.modelsv2.SimulateResponse.from_obj_for_encoding( - parseJsonWithBigints(rawSimulateResponse.toString('utf-8')) - ); - - const txnGroupDescriptorList = await TxnGroupSourceDescriptorList.loadFromFile( - fileAccessor, - txnGroupSourcesDescriptionPath - ); - - return new TEALDebuggingAssets(simulateResponse, txnGroupDescriptorList); - } + private _simulateResponse: algosdk.modelsv2.SimulateResponse; + private _txnGroupDescriptorList: TxnGroupSourceDescriptorList; + + constructor( + simulateResponse: algosdk.modelsv2.SimulateResponse, + txnGroupDescriptorList: TxnGroupSourceDescriptorList, + ) { + this._simulateResponse = simulateResponse; + this._txnGroupDescriptorList = txnGroupDescriptorList; + } + + public get simulateResponse(): algosdk.modelsv2.SimulateResponse { + return this._simulateResponse; + } + + public get txnGroupDescriptorList(): TxnGroupSourceDescriptorList { + return this._txnGroupDescriptorList; + } + + static async loadFromFiles( + fileAccessor: FileAccessor, + simulateResponsePath: string, + txnGroupSourcesDescriptionPath: string, + ): Promise { + const rawSimulateResponse = Buffer.from( + await fileAccessor.readFile(simulateResponsePath), + ); + const simulateResponse = + algosdk.modelsv2.SimulateResponse.from_obj_for_encoding( + parseJsonWithBigints(rawSimulateResponse.toString('utf-8')), + ); + + const txnGroupDescriptorList = + await TxnGroupSourceDescriptorList.loadFromFile( + fileAccessor, + txnGroupSourcesDescriptionPath, + ); + + return new TEALDebuggingAssets(simulateResponse, txnGroupDescriptorList); + } } diff --git a/src/extension.ts b/src/extension.ts index 5a79e9c..ba9cd05 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,118 +5,134 @@ import * as vscode from 'vscode'; import { ProviderResult } from 'vscode'; import { TxnGroupDebugSession } from './debugAdapter/debugRequestHandlers'; import { activateTealDebug, workspaceFileAccessor } from './activateMockDebug'; -import { TEALDebuggingAssetsDescriptor, loadTEALDAConfiguration } from './utils'; +import { + TEALDebuggingAssetsDescriptor, + loadTEALDAConfiguration, +} from './utils'; import { TEALDebuggingAssets } from './debugAdapter/utils'; const runMode: 'external' | 'server' = 'server'; export function activate(context: vscode.ExtensionContext) { - - // Load config for debug from launch.json here - // Error abort if failed to load - let config: vscode.DebugConfiguration | undefined = loadTEALDAConfiguration(); - if (typeof config === "undefined") { - // TODO: check if this is the best practice of aborting? - console.assert(0); - return; - } - - const debugAssetDescriptor = new TEALDebuggingAssetsDescriptor(config); - const debugAssets = TEALDebuggingAssets.loadFromFiles(workspaceFileAccessor, debugAssetDescriptor.simulateResponseFullPath.fsPath, debugAssetDescriptor.txnGroupSourceDescriptionFullPath.fsPath); - - switch (runMode) { - case 'server': - activateTealDebug( - context, new TEALDebugAdapterServerDescriptorFactory(debugAssets), config); - break; - - case 'external': default: - activateTealDebug(context, new TEALDebugAdapterExecutableFactory(debugAssetDescriptor), config); - break; - } + // Load config for debug from launch.json here + // Error abort if failed to load + const config: vscode.DebugConfiguration | undefined = + loadTEALDAConfiguration(); + if (typeof config === 'undefined') { + // TODO: check if this is the best practice of aborting? + console.assert(0); + return; + } + + const debugAssetDescriptor = new TEALDebuggingAssetsDescriptor(config); + const debugAssets = TEALDebuggingAssets.loadFromFiles( + workspaceFileAccessor, + debugAssetDescriptor.simulateResponseFullPath.fsPath, + debugAssetDescriptor.txnGroupSourceDescriptionFullPath.fsPath, + ); + + switch (runMode) { + case 'server': + activateTealDebug( + context, + new TEALDebugAdapterServerDescriptorFactory(debugAssets), + config, + ); + break; + + case 'external': + default: + activateTealDebug( + context, + new TEALDebugAdapterExecutableFactory(debugAssetDescriptor), + config, + ); + break; + } } -export function deactivate() { } +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function deactivate() {} export interface TEALDebugAdapterDescriptorFactory - extends vscode.DebugAdapterDescriptorFactory { - - dispose(): any + extends vscode.DebugAdapterDescriptorFactory { + dispose(); } class TEALDebugAdapterExecutableFactory - implements TEALDebugAdapterDescriptorFactory { - - // @ts-ignore TS6133 - private _debugAssetDescriptor: TEALDebuggingAssetsDescriptor; - - constructor(debugAssetDescriptor: TEALDebuggingAssetsDescriptor) { - this._debugAssetDescriptor = debugAssetDescriptor; - } - - createDebugAdapterDescriptor( - _session: vscode.DebugSession, - executable: vscode.DebugAdapterExecutable | undefined - ): ProviderResult { - // param "executable" contains the executable optionally specified in the package.json (if any) - - // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session) - - // TODO: IMPLEMENT HERE - if (!executable) { - const command = "absolute path to my DA executable"; - const args = [ - "some args", - "another arg" - ]; - const options = { - cwd: "working directory for executable", - env: { "envVariable": "some value" } - }; - executable = new vscode.DebugAdapterExecutable( - command, args, options - ); - } - - // make VS Code launch the DA executable - return executable; - } - - dispose() { } + implements TEALDebugAdapterDescriptorFactory +{ + private _debugAssetDescriptor: TEALDebuggingAssetsDescriptor; + + constructor(debugAssetDescriptor: TEALDebuggingAssetsDescriptor) { + this._debugAssetDescriptor = debugAssetDescriptor; + } + + createDebugAdapterDescriptor( + _session: vscode.DebugSession, + executable: vscode.DebugAdapterExecutable | undefined, + ): ProviderResult { + // param "executable" contains the executable optionally specified in the package.json (if any) + + // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session) + + // TODO: IMPLEMENT HERE + if (!executable) { + const command = 'absolute path to my DA executable'; + const args = ['some args', 'another arg']; + const options = { + cwd: 'working directory for executable', + env: { envVariable: 'some value' }, + }; + executable = new vscode.DebugAdapterExecutable(command, args, options); + } + + // Just to make the compiler stop complaining about an unused var + this._debugAssetDescriptor; + + // make VS Code launch the DA executable + return executable; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + dispose() {} } class TEALDebugAdapterServerDescriptorFactory - implements TEALDebugAdapterDescriptorFactory { - - private server?: Net.Server; - - private _debugAssets: Promise; - - constructor(debugAssets: Promise) { - this._debugAssets = debugAssets; - } - - async createDebugAdapterDescriptor( - _session: vscode.DebugSession, - _executable: vscode.DebugAdapterExecutable | undefined - ): Promise { - if (!this.server) { - const debugAssets = await this._debugAssets; - this.server = Net.createServer(socket => { - const session = new TxnGroupDebugSession(workspaceFileAccessor, debugAssets); - session.setRunAsServer(true); - session.start(socket as NodeJS.ReadableStream, socket); - }).listen(0); - } - - return new vscode.DebugAdapterServer( - (this.server.address() as Net.AddressInfo).port - ); - } - - dispose() { - if (this.server) { - this.server.close(); - } - } + implements TEALDebugAdapterDescriptorFactory +{ + private server?: Net.Server; + + private _debugAssets: Promise; + + constructor(debugAssets: Promise) { + this._debugAssets = debugAssets; + } + + async createDebugAdapterDescriptor( + _session: vscode.DebugSession, + _executable: vscode.DebugAdapterExecutable | undefined, + ): Promise { + if (!this.server) { + const debugAssets = await this._debugAssets; + this.server = Net.createServer((socket) => { + const session = new TxnGroupDebugSession( + workspaceFileAccessor, + debugAssets, + ); + session.setRunAsServer(true); + session.start(socket as NodeJS.ReadableStream, socket); + }).listen(0); + } + + return new vscode.DebugAdapterServer( + (this.server.address() as Net.AddressInfo).port, + ); + } + + dispose() { + if (this.server) { + this.server.close(); + } + } } diff --git a/src/utils.ts b/src/utils.ts index 7c052ca..72b903d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,128 +6,160 @@ import * as path from 'path'; import * as _ from 'lodash'; function vscodeVariables(string, recursive = false) { - console.assert(vscode.workspace.workspaceFolders); - - let workspaces: vscode.WorkspaceFolder[] = vscode.workspace.workspaceFolders; - - let workspace = workspaces.length ? workspaces[0] : null; - let activeFile = vscode.window.activeTextEditor?.document; - let absoluteFilePath: string = (activeFile?.uri.fsPath); - string = string.replace(/\${workspaceFolder}/g, workspace?.uri.fsPath); - string = string.replace(/\${workspaceFolderBasename}/g, workspace?.name); - string = string.replace(/\${file}/g, absoluteFilePath); - let activeWorkspace = workspace; - let relativeFilePath = absoluteFilePath; - for (let workspace of workspaces) { - if (absoluteFilePath.replace(workspace.uri.fsPath, '') !== absoluteFilePath) { - activeWorkspace = workspace; - relativeFilePath = absoluteFilePath.replace(workspace.uri.fsPath, '').substring(path.sep.length); - break; - } - } - let parsedPath = path.parse(absoluteFilePath); - string = string.replace(/\${fileWorkspaceFolder}/g, activeWorkspace?.uri.fsPath); - string = string.replace(/\${relativeFile}/g, relativeFilePath); - string = string.replace(/\${relativeFileDirname}/g, relativeFilePath.substring(0, relativeFilePath.lastIndexOf(path.sep))); - string = string.replace(/\${fileBasename}/g, parsedPath.base); - string = string.replace(/\${fileBasenameNoExtension}/g, parsedPath.name); - string = string.replace(/\${fileExtname}/g, parsedPath.ext); - string = string.replace(/\${fileDirname}/g, parsedPath.dir.substring(parsedPath.dir.lastIndexOf(path.sep) + 1)); - string = string.replace(/\${cwd}/g, parsedPath.dir); - string = string.replace(/\${pathSeparator}/g, path.sep); - // string = string.replace(/\${lineNumber}/g, vscode.window.activeTextEditor.selection.start.line + 1); - // string = string.replace(/\${selectedText}/g, vscode.window.activeTextEditor.document.getText(new vscode.Range(vscode.window.activeTextEditor.selection.start, vscode.window.activeTextEditor.selection.end))); - string = string.replace(/\${env:(.*?)}/g, function (variable) { - return process.env[variable.match(/\${env:(.*?)}/)[1]] || ''; - }); - string = string.replace(/\${config:(.*?)}/g, function (variable) { - return vscode.workspace.getConfiguration().get(variable.match(/\${config:(.*?)}/)[1], ''); - }); - - if (recursive && string.match(/\${(workspaceFolder|workspaceFolderBasename|fileWorkspaceFolder|relativeFile|fileBasename|fileBasenameNoExtension|fileExtname|fileDirname|cwd|pathSeparator|lineNumber|selectedText|env:(.*?)|config:(.*?))}/)) { - string = vscodeVariables(string, recursive); + console.assert(vscode.workspace.workspaceFolders); + + const workspaces: vscode.WorkspaceFolder[] = ( + vscode.workspace.workspaceFolders + ); + + const workspace = workspaces.length ? workspaces[0] : null; + const activeFile = vscode.window.activeTextEditor?.document; + const absoluteFilePath: string = activeFile?.uri.fsPath; + string = string.replace(/\${workspaceFolder}/g, workspace?.uri.fsPath); + string = string.replace(/\${workspaceFolderBasename}/g, workspace?.name); + string = string.replace(/\${file}/g, absoluteFilePath); + let activeWorkspace = workspace; + let relativeFilePath = absoluteFilePath; + for (const workspace of workspaces) { + if ( + absoluteFilePath.replace(workspace.uri.fsPath, '') !== absoluteFilePath + ) { + activeWorkspace = workspace; + relativeFilePath = absoluteFilePath + .replace(workspace.uri.fsPath, '') + .substring(path.sep.length); + break; } - return string; + } + const parsedPath = path.parse(absoluteFilePath); + string = string.replace( + /\${fileWorkspaceFolder}/g, + activeWorkspace?.uri.fsPath, + ); + string = string.replace(/\${relativeFile}/g, relativeFilePath); + string = string.replace( + /\${relativeFileDirname}/g, + relativeFilePath.substring(0, relativeFilePath.lastIndexOf(path.sep)), + ); + string = string.replace(/\${fileBasename}/g, parsedPath.base); + string = string.replace(/\${fileBasenameNoExtension}/g, parsedPath.name); + string = string.replace(/\${fileExtname}/g, parsedPath.ext); + string = string.replace( + /\${fileDirname}/g, + parsedPath.dir.substring(parsedPath.dir.lastIndexOf(path.sep) + 1), + ); + string = string.replace(/\${cwd}/g, parsedPath.dir); + string = string.replace(/\${pathSeparator}/g, path.sep); + // string = string.replace(/\${lineNumber}/g, vscode.window.activeTextEditor.selection.start.line + 1); + // string = string.replace(/\${selectedText}/g, vscode.window.activeTextEditor.document.getText(new vscode.Range(vscode.window.activeTextEditor.selection.start, vscode.window.activeTextEditor.selection.end))); + string = string.replace(/\${env:(.*?)}/g, function (variable) { + return process.env[variable.match(/\${env:(.*?)}/)[1]] || ''; + }); + string = string.replace(/\${config:(.*?)}/g, function (variable) { + return vscode.workspace + .getConfiguration() + .get(variable.match(/\${config:(.*?)}/)[1], ''); + }); + + if ( + recursive && + string.match( + /\${(workspaceFolder|workspaceFolderBasename|fileWorkspaceFolder|relativeFile|fileBasename|fileBasenameNoExtension|fileExtname|fileDirname|cwd|pathSeparator|lineNumber|selectedText|env:(.*?)|config:(.*?))}/, + ) + ) { + string = vscodeVariables(string, recursive); + } + return string; } /** * loadTEALDAConfiguration reads from launch.json for configuration, - * then checks for the first debug config unit containing + * then checks for the first debug config unit containing * { * 'type': typeString, * 'request': requestString * }. */ export function loadTEALDAConfiguration({ - typeString = "teal", - requestString = "launch" -}: { typeString?: string, requestString?: string; } = {}): - vscode.DebugConfiguration | undefined { - - const launchJson = vscode.workspace.getConfiguration('launch'); - - const configs: vscode.DebugConfiguration[] | undefined - = launchJson.get('configurations'); - if (!configs) { - vscode.window.showErrorMessage( - "TEAL Debug Plugin Error: load configurations from launch.json error" - ); - return undefined; - } - - console.assert(configs.length && configs.length > 0); - - for (let i = 0; i < configs.length; i++) { - if (_.isMatch(configs[i], { - "type": typeString, - "request": requestString, - })) { - return configs[i]; - } - } - + typeString = 'teal', + requestString = 'launch', +}: { typeString?: string; requestString?: string } = {}): + | vscode.DebugConfiguration + | undefined { + const launchJson = vscode.workspace.getConfiguration('launch'); + + const configs: vscode.DebugConfiguration[] | undefined = + launchJson.get('configurations'); + if (!configs) { vscode.window.showErrorMessage( - "TEAL Debug Plugin Error: launch.json configurations array did not contain relevant TEAL Debug configuration" + 'TEAL Debug Plugin Error: load configurations from launch.json error', ); return undefined; + } + + console.assert(configs.length && configs.length > 0); + + for (let i = 0; i < configs.length; i++) { + if ( + _.isMatch(configs[i], { + type: typeString, + request: requestString, + }) + ) { + return configs[i]; + } + } + + vscode.window.showErrorMessage( + 'TEAL Debug Plugin Error: launch.json configurations array did not contain relevant TEAL Debug configuration', + ); + return undefined; } export function absPathAgainstWorkspace(pathStr: string): vscode.Uri { - pathStr = vscodeVariables(pathStr); + pathStr = vscodeVariables(pathStr); - if (!path.isAbsolute(pathStr)) { - console.assert(vscode.workspace.workspaceFolders); - const workspaceFolders = vscode.workspace.workspaceFolders; + if (!path.isAbsolute(pathStr)) { + console.assert(vscode.workspace.workspaceFolders); + const workspaceFolders = ( + vscode.workspace.workspaceFolders + ); - console.assert(workspaceFolders.length > 0); - const workspaceFolderUri = workspaceFolders[0].uri; + console.assert(workspaceFolders.length > 0); + const workspaceFolderUri = workspaceFolders[0].uri; - return vscode.Uri.joinPath(workspaceFolderUri, pathStr); - } - return vscode.Uri.file(pathStr); + return vscode.Uri.joinPath(workspaceFolderUri, pathStr); + } + return vscode.Uri.file(pathStr); } export class TEALDebuggingAssetsDescriptor { - private _simulateRespFilesysFullPath: vscode.Uri; - private _txnGroupSourcesDescriptionFullPath: vscode.Uri; - - constructor(config: vscode.DebugConfiguration) { - console.assert(config.simulationTraceFile); - this._simulateRespFilesysFullPath = - absPathAgainstWorkspace(config.simulationTraceFile); - console.assert(config.appSourceDescriptionFile); - this._txnGroupSourcesDescriptionFullPath = - absPathAgainstWorkspace(config.appSourceDescriptionFile); - - vscode.window.showInformationMessage(this._simulateRespFilesysFullPath.fsPath); - vscode.window.showInformationMessage(this._txnGroupSourcesDescriptionFullPath.fsPath); - } + private _simulateRespFilesysFullPath: vscode.Uri; + private _txnGroupSourcesDescriptionFullPath: vscode.Uri; - public get simulateResponseFullPath(): vscode.Uri { - return this._simulateRespFilesysFullPath; - } + constructor(config: vscode.DebugConfiguration) { + console.assert(config.simulationTraceFile); + this._simulateRespFilesysFullPath = absPathAgainstWorkspace( + config.simulationTraceFile, + ); + console.assert(config.appSourceDescriptionFile); + this._txnGroupSourcesDescriptionFullPath = absPathAgainstWorkspace( + config.appSourceDescriptionFile, + ); - public get txnGroupSourceDescriptionFullPath(): vscode.Uri { - return this._txnGroupSourcesDescriptionFullPath; - } + vscode.window.showInformationMessage( + this._simulateRespFilesysFullPath.fsPath, + ); + vscode.window.showInformationMessage( + this._txnGroupSourcesDescriptionFullPath.fsPath, + ); + } + + public get simulateResponseFullPath(): vscode.Uri { + return this._simulateRespFilesysFullPath; + } + + public get txnGroupSourceDescriptionFullPath(): vscode.Uri { + return this._txnGroupSourcesDescriptionFullPath; + } } diff --git a/teal-language-configuration.json b/teal-language-configuration.json index 5d0d82c..c5e4f74 100644 --- a/teal-language-configuration.json +++ b/teal-language-configuration.json @@ -1,5 +1,5 @@ { - "comments": { - "lineComment": "//", - }, -} \ No newline at end of file + "comments": { + "lineComment": "//" + } +} diff --git a/tests/adapter.test.ts b/tests/adapter.test.ts index ad84c00..f49a14f 100644 --- a/tests/adapter.test.ts +++ b/tests/adapter.test.ts @@ -2,799 +2,1633 @@ import * as assert from 'assert'; import * as path from 'path'; import * as algosdk from 'algosdk'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { DebugClient } from './client'; -import { TEALDebuggingAssets, ByteArrayMap } from '../src/debugAdapter/utils'; -import { BasicServer } from '../src/debugAdapter/basicServer'; -import { - assertVariables, - advanceTo, - testFileAccessor, - DATA_ROOT, - DEBUG_CLIENT_PATH, -} from './testing'; +import { ByteArrayMap } from '../src/debugAdapter/utils'; +import { TestFixture, assertVariables, advanceTo, DATA_ROOT } from './testing'; describe('Debug Adapter Tests', () => { - - describe('general', () => { - let server: BasicServer; - let dc: DebugClient; - - beforeEach(async () => { - const debugAssets: TEALDebuggingAssets = await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json') - ); - server = new BasicServer(testFileAccessor, debugAssets); - - // dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal', { - // env: { - // ...process.env, - // /* eslint-disable @typescript-eslint/naming-convention */ - // ALGORAND_SIMULATION_RESPONSE_PATH: path.join(DATA_ROOT, 'state-changes-local-resp.json'), - // ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH: path.join(DATA_ROOT, 'state-changes-sources.json'), - // /* eslint-enable @typescript-eslint/naming-convention */ - // } - // }, true); - // await dc.start(); - dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); - await dc.start(server.port()); - }); - - afterEach(async () => { - await dc.stop(); - server.dispose(); - }); - - describe('basic', () => { - it('should produce error for unknown request', async () => { - let success: boolean; - try { - await dc.send('illegal_request'); - success = true; - } catch (err) { - success = false; - } - assert.strictEqual(success, false); - }); - }); - - describe('initialize', () => { - - it('should return supported features', () => { - return dc.initializeRequest().then(response => { - response.body = response.body || {}; - assert.strictEqual(response.body.supportsConfigurationDoneRequest, true); - }); - }); - - it('should produce error for invalid \'pathFormat\'', async () => { - let success: boolean; - try { - await dc.initializeRequest({ - adapterID: 'teal', - linesStartAt1: true, - columnsStartAt1: true, - pathFormat: 'url' - }); - success = true; - } catch (err) { - success = false; - } - assert.strictEqual(success, false); - }); - }); - - describe('launch', () => { - - it('should run program to the end', async () => { - const PROGRAM = path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'); - - await Promise.all([ - dc.configurationSequence(), - dc.launch({ program: PROGRAM }), - dc.waitForEvent('terminated') - ]); - }); - - it('should stop on entry', async () => { - const PROGRAM = path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'); - const ENTRY_LINE = 2; - - await Promise.all([ - dc.configurationSequence(), - dc.launch({ program: PROGRAM, stopOnEntry: true }), - dc.assertStoppedLocation('entry', { line: ENTRY_LINE } ) - ]); - }); - }); - - describe('setBreakpoints', () => { - - it('should stop on a breakpoint', async () => { - - const PROGRAM = path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'); - const BREAKPOINT_LINE = 2; - - await dc.hitBreakpoint({ program: PROGRAM }, { path: PROGRAM, line: BREAKPOINT_LINE }); - }); - }); - }); - - describe('Stack and scratch changes', () => { - let server: BasicServer; - let dc: DebugClient; - - beforeEach(async () => { - const debugAssets: TEALDebuggingAssets = await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - path.join(DATA_ROOT, 'stack-scratch/simulate-response.json'), - path.join(DATA_ROOT, 'stack-scratch/sources.json') - ); - server = new BasicServer(testFileAccessor, debugAssets); - - dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); - await dc.start(server.port()); - }); - - afterEach(async () => { - await dc.stop(); - server.dispose(); - }); - - it('should return variables correctly', async () => { - const PROGRAM = path.join(DATA_ROOT, 'stack-scratch/stack-scratch.teal'); - - await dc.hitBreakpoint({ program: PROGRAM }, { path: PROGRAM, line: 3 }); - - await assertVariables(dc, { - pc: 6, - stack: [ - 1005 - ], - scratch: new Map(), - }); - - await advanceTo(dc, { program: PROGRAM, line: 12 }); - - await assertVariables(dc, { - pc: 18, - stack: [ - 10 - ], - scratch: new Map(), - }); - - await advanceTo(dc, { program: PROGRAM, line: 22 }); - - await assertVariables(dc, { - pc: 34, - stack: [ - 10, - 0, - 0, - 0, - 0, - 0, - 0, - ], - scratch: new Map(), - }); - - await advanceTo(dc, { program: PROGRAM, line: 35 }); - - await assertVariables(dc, { - pc: 63, - stack: [ - 10, - 30, - Buffer.from("1!"), - Buffer.from("5!"), - ], - scratch: new Map(), - }); - - await advanceTo(dc, { program: PROGRAM, line: 36 }); - - await assertVariables(dc, { - pc: 80, - stack: [ - 10, - 30, - Buffer.from("1!"), - Buffer.from("5!"), - 0, - 2, - 1, - 1, - 5, - BigInt('18446744073709551615') - ], - scratch: new Map(), - }); - - await advanceTo(dc, { program: PROGRAM, line: 37 }); - - await assertVariables(dc, { - pc: 82, - stack: [ - 10, - 30, - Buffer.from("1!"), - Buffer.from("5!"), - 0, - 2, - 1, - 1, - 5, - ], - scratch: new Map([ - [ - 1, - BigInt('18446744073709551615') - ], - ]), - }); - - await advanceTo(dc, { program: PROGRAM, line: 39 }); - - await assertVariables(dc, { - pc: 85, - stack: [ - 10, - 30, - Buffer.from("1!"), - Buffer.from("5!"), - 0, - 2, - 1, - 1, - ], - scratch: new Map([ - [ - 1, - BigInt('18446744073709551615') - ], - [ - 5, - BigInt('18446744073709551615') - ], - ]), - }); - - await advanceTo(dc, { program: PROGRAM, line: 41 }); - - await assertVariables(dc, { - pc: 89, - stack: [ - 10, - 30, - Buffer.from("1!"), - Buffer.from("5!"), - 0, - 2, - 1, - 1, - ], - scratch: new Map([ - [ - 1, - BigInt('18446744073709551615') - ], - [ - 5, - BigInt('18446744073709551615') - ], - ]), - }); - - await advanceTo(dc, { program: PROGRAM, line: 13 }); - - await assertVariables(dc, { - pc: 21, - stack: [ - 30, - ], - scratch: new Map([ - [ - 1, - BigInt('18446744073709551615') - ], - [ - 5, - BigInt('18446744073709551615') - ], - ]), - }); - }); - }); - - describe('Global state changes', () => { - let server: BasicServer; - let dc: DebugClient; - - beforeEach(async () => { - const debugAssets: TEALDebuggingAssets = await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - path.join(DATA_ROOT, 'app-state-changes/global-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json') - ); - server = new BasicServer(testFileAccessor, debugAssets); - - dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); - await dc.start(server.port()); - }); - - afterEach(async () => { - await dc.stop(); - server.dispose(); - }); - - it('should return variables correctly', async () => { - const PROGRAM = path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'); - - await dc.hitBreakpoint({ program: PROGRAM }, { path: PROGRAM, line: 3 }); - - await assertVariables(dc, { - pc: 6, - stack: [ - 1050 - ], - apps: [{ - appID: 1050, - globalState: new ByteArrayMap() - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 14 }); - - await assertVariables(dc, { - pc: 37, - stack: [ - Buffer.from('8e169311', 'hex'), - Buffer.from('8913c1f8', 'hex'), - Buffer.from('d513c44e', 'hex'), - Buffer.from('8913c1f8', 'hex'), - ], - apps: [{ - appID: 1050, - globalState: new ByteArrayMap() - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 31 }); - - await assertVariables(dc, { - pc: 121, - stack: [ - Buffer.from('global-int-key'), - 0xdeadbeef, - ], - apps: [{ - appID: 1050, - globalState: new ByteArrayMap() - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 32 }); - - await assertVariables(dc, { - pc: 122, - stack: [], - apps: [{ - appID: 1050, - globalState: new ByteArrayMap([ - [ - Buffer.from('global-int-key'), - 0xdeadbeef, - ], - ]) - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 35 }); - - await assertVariables(dc, { - pc: 156, - stack: [], - apps: [{ - appID: 1050, - globalState: new ByteArrayMap([ - [ - Buffer.from('global-int-key'), - 0xdeadbeef, - ], - [ - Buffer.from('global-bytes-key'), - Buffer.from('welt am draht'), - ] - ]) - }], - }); - }); - }); - - describe('Local state changes', () => { - let server: BasicServer; - let dc: DebugClient; - - beforeEach(async () => { - const debugAssets: TEALDebuggingAssets = await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json') - ); - server = new BasicServer(testFileAccessor, debugAssets); - - dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); - await dc.start(server.port()); - }); - - afterEach(async () => { - await dc.stop(); - server.dispose(); - }); - - it('should return variables correctly', async () => { - const PROGRAM = path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'); - - await dc.hitBreakpoint({ program: PROGRAM }, { path: PROGRAM, line: 3 }); - - await assertVariables(dc, { - pc: 6, - stack: [ - 1054 - ], - apps: [{ - appID: 1054, - localState: [{ - account: 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', - state: new ByteArrayMap(), - }], - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 14 }); - - await assertVariables(dc, { - pc: 37, - stack: [ - Buffer.from('8e169311', 'hex'), - Buffer.from('8913c1f8', 'hex'), - Buffer.from('d513c44e', 'hex'), - Buffer.from('8e169311', 'hex'), - ], - apps: [{ - appID: 1054, - localState: [{ - account: 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', - state: new ByteArrayMap(), - }], - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 21 }); - - await assertVariables(dc, { - pc: 69, - stack: [ - algosdk.decodeAddress('YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4').publicKey, - Buffer.from('local-int-key'), - 0xcafeb0ba, - ], - apps: [{ - appID: 1054, - localState: [{ - account: 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', - state: new ByteArrayMap(), - }], - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 22 }); - - await assertVariables(dc, { - pc: 70, - stack: [], - apps: [{ - appID: 1054, - localState: [{ - account: 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', - state: new ByteArrayMap([ - [ - Buffer.from('local-int-key'), - 0xcafeb0ba, - ], - ]), - }], - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 26 }); - - await assertVariables(dc, { - pc: 96, - stack: [], - apps: [{ - appID: 1054, - localState: [{ - account: 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', - state: new ByteArrayMap([ - [ - Buffer.from('local-int-key'), - 0xcafeb0ba, - ], - [ - Buffer.from('local-bytes-key'), - Buffer.from('xqcL'), - ], - ]), - }], - }], - }); - }); - }); - - describe('Box state changes', () => { - let server: BasicServer; - let dc: DebugClient; - - beforeEach(async () => { - const debugAssets: TEALDebuggingAssets = await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - path.join(DATA_ROOT, 'app-state-changes/box-simulate-response.json'), - path.join(DATA_ROOT, 'app-state-changes/sources.json') - ); - server = new BasicServer(testFileAccessor, debugAssets); - - dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); - await dc.start(server.port()); - }); - - afterEach(async () => { - await dc.stop(); - server.dispose(); - }); - - it('should return variables correctly', async () => { - const PROGRAM = path.join(DATA_ROOT, 'app-state-changes/state-changes.teal'); - - await dc.hitBreakpoint({ program: PROGRAM }, { path: PROGRAM, line: 3 }); - - await assertVariables(dc, { - pc: 6, - stack: [ - 1058 - ], - apps: [{ - appID: 1058, - boxState: new ByteArrayMap() - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 14 }); - - await assertVariables(dc, { - pc: 37, - stack: [ - Buffer.from('8e169311', 'hex'), - Buffer.from('8913c1f8', 'hex'), - Buffer.from('d513c44e', 'hex'), - Buffer.from('d513c44e', 'hex'), - ], - apps: [{ - appID: 1058, - boxState: new ByteArrayMap() - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 40 }); - - await assertVariables(dc, { - pc: 183, - stack: [ - Buffer.from('box-key-1'), - Buffer.from('box-value-1'), - ], - apps: [{ - appID: 1058, - boxState: new ByteArrayMap() - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 41 }); - - await assertVariables(dc, { - pc: 184, - stack: [], - apps: [{ - appID: 1058, - boxState: new ByteArrayMap([ - [ - Buffer.from('box-key-1'), - Buffer.from('box-value-1'), - ], - ]) - }], - }); - - await advanceTo(dc, { program: PROGRAM, line: 46 }); - - await assertVariables(dc, { - pc: 198, - stack: [], - apps: [{ - appID: 1058, - boxState: new ByteArrayMap([ - [ - Buffer.from('box-key-1'), - Buffer.from('box-value-1'), - ], - [ - Buffer.from('box-key-2'), - Buffer.from(''), - ] - ]) - }], - }); - }); - }); - - describe('Source mapping', () => { - let server: BasicServer; - let dc: DebugClient; - - beforeEach(async () => { - const debugAssets: TEALDebuggingAssets = await TEALDebuggingAssets.loadFromFiles( - testFileAccessor, - path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), - path.join(DATA_ROOT, 'sourcemap-test/sources.json') - ); - server = new BasicServer(testFileAccessor, debugAssets); - - dc = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); - await dc.start(server.port()); - }); - - afterEach(async () => { - await dc.stop(); - server.dispose(); - }); - - interface SourceInfo { - path: string, - validBreakpoints: DebugProtocol.BreakpointLocation[], - } - - const testSources: SourceInfo[] = [ - { - path: path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), - validBreakpoints: [ - { line: 4, column: 1 }, - { line: 4, column: 20 }, - { line: 4, column: 27 }, - { line: 4, column: 31 }, - { line: 7, column: 5 }, - { line: 7, column: 12 }, - { line: 7, column: 19 }, - { line: 8, column: 5 }, - { line: 8, column: 12 }, - { line: 8, column: 19 }, - { line: 12, column: 5 }, - { line: 13, column: 5 }, - ] - }, - { - path: path.join(DATA_ROOT, 'sourcemap-test/lib.teal'), - validBreakpoints: [ - { line: 2, column: 22 }, - { line: 2, column: 26 }, - ] - }, - ]; - - it('should return correct breakpoint locations', async () => { - for (const source of testSources) { - const response = await dc.breakpointLocationsRequest({ - source: { - path: source.path, - }, - line: 0, - endLine: 100, - }); - assert.ok(response.success); - - const actualBreakpointLocations = response.body.breakpoints.slice(); - // Sort the response so that it's easier to compare - actualBreakpointLocations.sort((a, b) => { - if (a.line === b.line) { - return (a.column ?? 0) - (b.column ?? 0); - } - return a.line - b.line; - }); - - assert.deepStrictEqual(actualBreakpointLocations, source.validBreakpoints); - } - }); - - it('should correctly set and stop at valid breakpoints', async () => { - await Promise.all([ - dc.configurationSequence(), - dc.launch({ - program: path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), - stopOnEntry: true - }), - dc.assertStoppedLocation('entry', {}) - ]); - - for (const source of testSources) { - const result = await dc.setBreakpointsRequest({ - source: { path: source.path }, - breakpoints: source.validBreakpoints, - }); - assert.ok(result.success); - - assert.ok(result.body.breakpoints.every(bp => bp.verified)); - const actualBreakpointLocations = result.body.breakpoints - .map(bp => ({ line: bp.line, column: bp.column })); - assert.deepStrictEqual(actualBreakpointLocations, source.validBreakpoints); - } - - // The breakpoints will not necessarily be hit in order, since PCs map to different - // places in the source file, so we will keep track of which breakpoints have been hit. - const seenBreakpointLocation: boolean[][] = testSources.map(source => source.validBreakpoints.map(() => false)); - - while (seenBreakpointLocation.some(sourceBreakpoints => sourceBreakpoints.some(seen => !seen))) { - await dc.continueRequest({ threadId: 1 }); - const stoppedResponse = await dc.assertStoppedLocation('breakpoint', {}); - const stoppedFrame = stoppedResponse.body.stackFrames[0]; - let found = false; - for (let sourceIndex = 0; sourceIndex < testSources.length; sourceIndex++) { - const source = testSources[sourceIndex]; - if (source.path !== stoppedFrame.source?.path) { - continue; - } - for (let i = 0; i < source.validBreakpoints.length; i++) { - if (source.validBreakpoints[i].line === stoppedFrame.line && - source.validBreakpoints[i].column === stoppedFrame.column) { - assert.strictEqual(seenBreakpointLocation[sourceIndex][i], false, `Breakpoint ${i} was hit twice. Line: ${stoppedFrame.line}, Column: ${stoppedFrame.column}, Path: ${source.path}`); - seenBreakpointLocation[sourceIndex][i] = true; - found = true; - break; - } - } - } - assert.ok(found, `Breakpoint at path ${stoppedFrame.source?.path}, line ${stoppedFrame.line}, column ${stoppedFrame.column} was not expected`); - } - }); - - it('should correctly handle invalid breakpoints and not stop at them', async () => { - await Promise.all([ - dc.configurationSequence(), - dc.launch({ - program: path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), - stopOnEntry: true - }), - dc.assertStoppedLocation('entry', {}) - ]); - - const result = await dc.setBreakpointsRequest({ - source: { path: path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal') }, - breakpoints: [ - { line: 0, column: 0 }, - { line: 100, column: 0 }, - { line: 0, column: 100 }, - { line: 100, column: 100 }, - ], - }); - assert.ok(result.success); - - assert.ok(result.body.breakpoints.every(bp => !bp.verified)); - - await Promise.all([ - dc.continueRequest({ threadId: 1 }), - dc.waitForEvent('terminated') - ]); - }); - }); + const fixture = new TestFixture(); + + afterEach(async () => { + await fixture.reset(); + }); + + describe('general', () => { + beforeEach(async () => { + await fixture.init( + path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'), + path.join(DATA_ROOT, 'app-state-changes/sources.json'), + ); + }); + + describe('basic', () => { + it('should produce error for unknown request', async () => { + let success: boolean; + try { + await fixture.client.send('illegal_request'); + success = true; + } catch (err) { + success = false; + } + assert.strictEqual(success, false); + }); + }); + + describe('initialize', () => { + it('should return supported features', () => { + return fixture.client.initializeRequest().then((response) => { + response.body = response.body || {}; + assert.strictEqual( + response.body.supportsConfigurationDoneRequest, + true, + ); + }); + }); + + it("should produce error for invalid 'pathFormat'", async () => { + let success: boolean; + try { + await fixture.client.initializeRequest({ + adapterID: 'teal', + linesStartAt1: true, + columnsStartAt1: true, + pathFormat: 'url', + }); + success = true; + } catch (err) { + success = false; + } + assert.strictEqual(success, false); + }); + }); + + describe('launch', () => { + it('should run program to the end', async () => { + const PROGRAM = path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ); + + await Promise.all([ + fixture.client.configurationSequence(), + fixture.client.launch({ program: PROGRAM }), + fixture.client.waitForEvent('terminated'), + ]); + }); + + it('should stop on entry', async () => { + const PROGRAM = path.join( + DATA_ROOT, + 'app-state-changes/local-simulate-response.json', + ); + const ENTRY_LINE = 2; + + await Promise.all([ + fixture.client.configurationSequence(), + fixture.client.launch({ program: PROGRAM, stopOnEntry: true }), + fixture.client.assertStoppedLocation('entry', { line: ENTRY_LINE }), + ]); + }); + }); + + describe('setBreakpoints', () => { + it('should stop on a breakpoint', async () => { + const PROGRAM = path.join( + DATA_ROOT, + 'app-state-changes/state-changes.teal', + ); + const BREAKPOINT_LINE = 2; + + await fixture.client.hitBreakpoint( + { program: PROGRAM }, + { path: PROGRAM, line: BREAKPOINT_LINE }, + ); + }); + }); + }); + + describe('Controls', () => { + interface Location { + program?: string; + name: string; + line: number; + column: number; + } + + const expectedStepOverLocationsSteppingTest: Location[] = [ + { + name: 'transaction-group-0.json', + line: 2, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 18, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 19, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 23, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 33, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 34, + column: 0, + }, + ]; + + let expectedStepOverLocationsSlotMachine: Location[]; + { + const slotMachinePath = path.join( + DATA_ROOT, + 'slot-machine/slot-machine.teal', + ); + + const label5Callsub = [ + { line: 97, column: 1 }, + { line: 98, column: 1 }, + { line: 99, column: 1 }, + { line: 100, column: 1 }, + { line: 101, column: 1 }, + ]; + + const label6Callsub = [ + { line: 103, column: 1 }, + { line: 104, column: 1 }, + { line: 105, column: 1 }, + { line: 106, column: 1 }, + { line: 107, column: 1 }, + { line: 108, column: 1 }, + { line: 109, column: 1 }, + { line: 110, column: 1 }, + { line: 111, column: 1 }, + ]; + + expectedStepOverLocationsSlotMachine = [ + { line: 2, column: 1 }, + { line: 3, column: 1 }, + { line: 4, column: 1 }, + { line: 5, column: 1 }, + { line: 6, column: 1 }, + { line: 7, column: 1 }, + { line: 8, column: 1 }, + { line: 9, column: 1 }, + { line: 10, column: 1 }, + { line: 11, column: 1 }, + { line: 12, column: 1 }, + { line: 13, column: 1 }, + { line: 14, column: 1 }, + { line: 15, column: 1 }, + { line: 16, column: 1 }, + { line: 17, column: 1 }, + { line: 18, column: 1 }, + { line: 20, column: 1 }, + { line: 21, column: 1 }, + { line: 22, column: 1 }, + { line: 23, column: 1 }, + { line: 24, column: 1 }, + { line: 25, column: 1 }, + { line: 26, column: 1 }, + { line: 27, column: 1 }, + { line: 28, column: 1 }, + { line: 29, column: 1 }, + { line: 30, column: 1 }, + { line: 31, column: 1 }, + { line: 32, column: 1 }, + { line: 33, column: 1 }, + { line: 34, column: 1 }, + { line: 35, column: 1 }, + { line: 36, column: 1 }, + { line: 37, column: 1 }, + { line: 38, column: 1 }, + { line: 39, column: 1 }, + { line: 40, column: 1 }, + { line: 41, column: 1 }, + { line: 42, column: 1 }, + { line: 43, column: 1 }, + { line: 44, column: 1 }, + { line: 45, column: 1 }, + { line: 46, column: 1 }, + { line: 20, column: 1 }, + { line: 21, column: 1 }, + { line: 22, column: 1 }, + { line: 23, column: 1 }, + { line: 24, column: 1 }, + { line: 25, column: 1 }, + { line: 26, column: 1 }, + { line: 27, column: 1 }, + { line: 28, column: 1 }, + { line: 29, column: 1 }, + { line: 30, column: 1 }, + { line: 31, column: 1 }, + { line: 32, column: 1 }, + { line: 33, column: 1 }, + { line: 34, column: 1 }, + { line: 35, column: 1 }, + { line: 36, column: 1 }, + { line: 37, column: 1 }, + { line: 38, column: 1 }, + { line: 39, column: 1 }, + { line: 40, column: 1 }, + { line: 41, column: 1 }, + { line: 42, column: 1 }, + { line: 43, column: 1 }, + { line: 44, column: 1 }, + { line: 45, column: 1 }, + { line: 46, column: 1 }, + { line: 20, column: 1 }, + { line: 21, column: 1 }, + { line: 22, column: 1 }, + { line: 23, column: 1 }, + { line: 24, column: 1 }, + { line: 25, column: 1 }, + { line: 26, column: 1 }, + { line: 27, column: 1 }, + { line: 28, column: 1 }, + { line: 29, column: 1 }, + { line: 30, column: 1 }, + { line: 31, column: 1 }, + { line: 32, column: 1 }, + { line: 33, column: 1 }, + { line: 34, column: 1 }, + { line: 35, column: 1 }, + { line: 36, column: 1 }, + { line: 37, column: 1 }, + { line: 38, column: 1 }, + { line: 39, column: 1 }, + { line: 40, column: 1 }, + { line: 41, column: 1 }, + { line: 42, column: 1 }, + { line: 43, column: 1 }, + { line: 44, column: 1 }, + { line: 48, column: 1 }, + { line: 49, column: 1 }, + { line: 50, column: 1 }, + { line: 51, column: 1 }, + { line: 52, column: 1 }, + { line: 53, column: 1 }, + ...label5Callsub, + { line: 54, column: 1 }, + { line: 55, column: 1 }, + { line: 56, column: 1 }, + ...label6Callsub, + { line: 57, column: 1 }, + { line: 58, column: 1 }, + { line: 59, column: 1 }, + ...label6Callsub, + { line: 60, column: 1 }, + { line: 61, column: 1 }, + { line: 62, column: 1 }, + ...label5Callsub, + { line: 63, column: 1 }, + { line: 64, column: 1 }, + { line: 65, column: 1 }, + ...label6Callsub, + { line: 66, column: 1 }, + { line: 67, column: 1 }, + { line: 68, column: 1 }, + ...label6Callsub, + { line: 69, column: 1 }, + { line: 70, column: 1 }, + { line: 71, column: 1 }, + ...label5Callsub, + { line: 72, column: 1 }, + { line: 73, column: 1 }, + { line: 74, column: 1 }, + ...label6Callsub, + { line: 75, column: 1 }, + { line: 76, column: 1 }, + { line: 77, column: 1 }, + ...label6Callsub, + { line: 78, column: 1 }, + { line: 79, column: 1 }, + { line: 80, column: 1 }, + { line: 81, column: 1 }, + { line: 82, column: 1 }, + { line: 83, column: 1 }, + { line: 84, column: 1 }, + { line: 85, column: 1 }, + { line: 86, column: 1 }, + { line: 87, column: 1 }, + { line: 88, column: 1 }, + { line: 89, column: 1 }, + { line: 90, column: 1 }, + { line: 91, column: 1 }, + { line: 92, column: 1 }, + { line: 93, column: 1 }, + { line: 94, column: 1 }, + { line: 95, column: 1 }, + ].map((partial) => ({ + ...partial, + name: 'slot-machine.teal', + program: slotMachinePath, + })); + } + + describe('Step in', () => { + it('should pause at the correct locations', async () => { + const simulateTracePath = path.join( + DATA_ROOT, + 'stepping-test/simulate-response.json', + ); + await fixture.init( + simulateTracePath, + path.join(DATA_ROOT, 'stepping-test/sources.json'), + ); + const { client } = fixture; + + await Promise.all([ + client.configurationSequence(), + client.launch({ program: simulateTracePath, stopOnEntry: true }), + client.assertStoppedLocation('entry', {}), + ]); + + const lsigPath = path.join(DATA_ROOT, 'stepping-test/lsig.teal'); + const appPath = path.join(DATA_ROOT, 'stepping-test/app.teal'); + const expectedLocations: Location[] = [ + { + name: 'transaction-group-0.json', + line: 2, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 18, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 19, + column: 0, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 2, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 3, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 4, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 5, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 6, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 7, + column: 1, + }, + { + name: 'transaction-group-0.json', + line: 23, + column: 0, + }, + { + program: appPath, + name: 'app.teal', + line: 2, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 3, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 5, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 12, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 13, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 14, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 8, + column: 1, + }, + { + program: appPath, + name: 'app.teal', + line: 9, + column: 1, + }, + { + name: 'transaction-group-0.json', + line: 33, + column: 0, + }, + { + name: 'transaction-group-0.json', + line: 34, + column: 0, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 2, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 3, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 4, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 5, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 6, + column: 1, + }, + { + program: lsigPath, + name: 'lsig.teal', + line: 7, + column: 1, + }, + ]; + + for (let i = 0; i < expectedLocations.length; i++) { + const expectedLocation = expectedLocations[i]; + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + const actualLocation: Location = { + name: currentFrame.source?.name!, + line: currentFrame.line, + column: currentFrame.column, + }; + if (currentFrame.source?.path) { + actualLocation.program = currentFrame.source.path; + } + assert.deepStrictEqual(actualLocation, expectedLocation); + + // Move to next location + await client.stepInRequest({ threadId: 1 }); + if (i + 1 < expectedLocations.length) { + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + } else { + await client.waitForEvent('terminated'); + } + } + }); + }); + + describe('Step over', () => { + it('should pause at the correct locations in a transaction group', async () => { + const simulateTracePath = path.join( + DATA_ROOT, + 'stepping-test/simulate-response.json', + ); + await fixture.init( + simulateTracePath, + path.join(DATA_ROOT, 'stepping-test/sources.json'), + ); + const { client } = fixture; + + await Promise.all([ + client.configurationSequence(), + client.launch({ program: simulateTracePath, stopOnEntry: true }), + client.assertStoppedLocation('entry', {}), + ]); + + const expectedLocations = expectedStepOverLocationsSteppingTest; + + for (let i = 0; i < expectedLocations.length; i++) { + const expectedLocation = expectedLocations[i]; + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + const actualLocation: Location = { + name: currentFrame.source?.name!, + line: currentFrame.line, + column: currentFrame.column, + }; + if (currentFrame.source?.path) { + actualLocation.program = currentFrame.source.path; + } + assert.deepStrictEqual(actualLocation, expectedLocation); + + // Move to next location + await client.nextRequest({ threadId: 1 }); + if (i + 1 < expectedLocations.length) { + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + } else { + await client.waitForEvent('terminated'); + } + } + }); + + it('should pause at the correct locations in app execution', async () => { + const simulateTracePath = path.join( + DATA_ROOT, + 'slot-machine/simulate-response.json', + ); + await fixture.init( + simulateTracePath, + path.join(DATA_ROOT, 'slot-machine/sources.json'), + ); + const { client } = fixture; + + const programPath = path.join( + DATA_ROOT, + 'slot-machine/slot-machine.teal', + ); + + await client.hitBreakpoint( + { program: simulateTracePath }, + { path: programPath, line: 2 }, + ); + + const expectedLocations = expectedStepOverLocationsSlotMachine; + + for (let i = 0; i < expectedLocations.length; i++) { + const expectedLocation = expectedLocations[i]; + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + const actualLocation: Location = { + name: currentFrame.source?.name!, + line: currentFrame.line, + column: currentFrame.column, + }; + if (currentFrame.source?.path) { + actualLocation.program = currentFrame.source.path; + } + assert.deepStrictEqual(actualLocation, expectedLocation); + + // Move to next location + await client.nextRequest({ threadId: 1 }); + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + } + + // Finally, assert that the next step is not in the program + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + assert.notStrictEqual( + currentFrame.source?.path, + programPath, + 'Program has step locations beyond expected', + ); + assert.notStrictEqual( + currentFrame.source?.name, + 'slot-machine.teal', + 'Program has step locations beyond expected', + ); + }); + }); + + describe('Step out', () => { + it('should pause at the correct locations', async () => { + const simulateTracePath = path.join( + DATA_ROOT, + 'slot-machine/simulate-response.json', + ); + await fixture.init( + simulateTracePath, + path.join(DATA_ROOT, 'slot-machine/sources.json'), + ); + const { client } = fixture; + + const fakeRandomPath = path.join( + DATA_ROOT, + 'slot-machine/fake-random.teal', + ); + const randomBytePath = path.join( + DATA_ROOT, + 'slot-machine/random-byte.teal', + ); + const slotMachinePath = path.join( + DATA_ROOT, + 'slot-machine/slot-machine.teal', + ); + + await client.hitBreakpoint( + { program: simulateTracePath }, + { path: fakeRandomPath, line: 13 }, + ); + + // clear breakpoint + await client.setBreakpointsRequest({ + source: { path: fakeRandomPath }, + breakpoints: [], + }); + + interface LocationAndFrameState { + location: Location; + frameStates: Array<{ + pc: number; + stack: Array; + } | null>; + } + + const expectedLocations: LocationAndFrameState[] = [ + { + location: { + program: fakeRandomPath, + name: 'fake-random.teal', + line: 13, + column: 1, + }, + frameStates: [ + { + pc: 33, + stack: [ + Buffer.from('0000000001fa5f5d23', 'hex'), + Buffer.from('counter'), + ], + }, + null, + { + pc: 45, + stack: [], + }, + null, + { + pc: 108, + stack: [], + }, + null, + ], + }, + { + location: { + program: randomBytePath, + name: 'random-byte.teal', + line: 22, + column: 1, + }, + frameStates: [ + { + pc: 46, + stack: [], + }, + null, + { + pc: 108, + stack: [], + }, + null, + ], + }, + { + location: { + name: 'inner-transaction-group-0-0.json', + line: 20, + column: 0, + }, + frameStates: [ + null, + { + pc: 108, + stack: [], + }, + null, + ], + }, + { + location: { + program: slotMachinePath, + name: 'slot-machine.teal', + line: 52, + column: 1, + }, + frameStates: [ + { + pc: 109, + stack: [], + }, + null, + ], + }, + { + location: { + name: 'transaction-group-0.json', + line: 40, + column: 0, + }, + frameStates: [null], + }, + ]; + + for (let i = 0; i < expectedLocations.length; i++) { + const expectedLocation = expectedLocations[i]; + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + const actualLocation: Location = { + name: currentFrame.source?.name!, + line: currentFrame.line, + column: currentFrame.column, + }; + if (currentFrame.source?.path) { + actualLocation.program = currentFrame.source.path; + } + assert.deepStrictEqual(actualLocation, expectedLocation.location); + + assert.strictEqual( + stackTraceResponse.body.stackFrames.length, + expectedLocation.frameStates.length, + ); + + for ( + let frameIndex = 0; + frameIndex < expectedLocation.frameStates.length; + frameIndex++ + ) { + const expectedFrameState = expectedLocation.frameStates[frameIndex]; + const frameId = stackTraceResponse.body.stackFrames[frameIndex].id; + + if (expectedFrameState) { + await assertVariables(client, expectedFrameState, frameId); + } else { + const scopesResponse = await client.scopesRequest({ frameId }); + assert.ok(scopesResponse.success); + const scopes = scopesResponse.body.scopes; + + const executionScope = scopes.find((scope) => + scope.name.startsWith('Program State'), + ); + assert.strictEqual(executionScope, undefined); + } + } + + // Move to next location + await client.stepOutRequest({ threadId: 1 }); + if (i + 1 < expectedLocations.length) { + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + } else { + await client.waitForEvent('terminated'); + } + } + }); + }); + + describe('Step back', () => { + it('should pause at the correct locations in a transaction group', async () => { + const simulateTracePath = path.join( + DATA_ROOT, + 'stepping-test/simulate-response.json', + ); + await fixture.init( + simulateTracePath, + path.join(DATA_ROOT, 'stepping-test/sources.json'), + ); + const { client } = fixture; + + await Promise.all([ + client.configurationSequence(), + client.launch({ program: simulateTracePath, stopOnEntry: true }), + client.assertStoppedLocation('entry', {}), + ]); + + const expectedLocations = expectedStepOverLocationsSteppingTest + .slice() + .reverse(); + + // Can't set breakpoints on the transaction-group-0.json pseudo file, so let's keep + // stepping until we reach our starting location. + const startLocation = expectedLocations[0]; + for (;;) { + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + if ( + currentFrame.source?.name === startLocation.name && + currentFrame.line === startLocation.line + ) { + break; + } + await client.nextRequest({ threadId: 1 }); + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + } + + for (let i = 0; i < expectedLocations.length; i++) { + const expectedLocation = expectedLocations[i]; + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + const actualLocation: Location = { + name: currentFrame.source?.name!, + line: currentFrame.line, + column: currentFrame.column, + }; + if (currentFrame.source?.path) { + actualLocation.program = currentFrame.source.path; + } + assert.deepStrictEqual(actualLocation, expectedLocation); + + // Move to next location, in this case backwards + await client.stepBackRequest({ threadId: 1 }); + const stoppedEvent = await client.waitForStop(); + const expectedStopReason = + i + 1 === expectedLocations.length ? 'entry' : 'step'; + assert.strictEqual(stoppedEvent.body.reason, expectedStopReason); + } + }); + + it('should pause at the correct locations in app execution', async () => { + const simulateTracePath = path.join( + DATA_ROOT, + 'slot-machine/simulate-response.json', + ); + await fixture.init( + simulateTracePath, + path.join(DATA_ROOT, 'slot-machine/sources.json'), + ); + const { client } = fixture; + + const expectedLocations = expectedStepOverLocationsSlotMachine + .slice() + .reverse(); + + const startLocation = expectedLocations[0]; + await client.hitBreakpoint( + { program: simulateTracePath }, + { + path: startLocation.program!, + line: startLocation.line, + column: startLocation.column, + }, + ); + + for (let i = 0; i < expectedLocations.length; i++) { + const expectedLocation = expectedLocations[i]; + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + const actualLocation: Location = { + name: currentFrame.source?.name!, + line: currentFrame.line, + column: currentFrame.column, + }; + if (currentFrame.source?.path) { + actualLocation.program = currentFrame.source.path; + } + assert.deepStrictEqual(actualLocation, expectedLocation); + + // Move to next location, in this case backwards + await client.stepBackRequest({ threadId: 1 }); + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + } + + // Finally, assert that the next step is not in the program + const stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + const currentFrame = stackTraceResponse.body.stackFrames[0]; + assert.notStrictEqual( + currentFrame.source?.path, + path.join(DATA_ROOT, 'slot-machine/slot-machine.teal'), + 'Program has step locations beyond expected', + ); + assert.notStrictEqual( + currentFrame.source?.name, + 'slot-machine.teal', + 'Program has step locations beyond expected', + ); + }); + }); + }); + + describe('Stack and scratch changes', () => { + context('LogicSig', () => { + it('should return variables correctly', async () => { + const simulateResponse = path.join( + DATA_ROOT, + 'stepping-test/simulate-response.json', + ); + await fixture.init( + simulateResponse, + path.join(DATA_ROOT, 'stepping-test/sources.json'), + ); + + const { client } = fixture; + const PROGRAM = path.join(DATA_ROOT, 'stepping-test/lsig.teal'); + + await client.hitBreakpoint( + { program: simulateResponse }, + { path: PROGRAM, line: 3 }, + ); + + await assertVariables(client, { + pc: 3, + stack: [0], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 5 }); + + await assertVariables(client, { + pc: 6, + stack: [1, new Uint8Array(32)], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 7 }); + + await assertVariables(client, { + pc: 9, + stack: [1, 1], + scratch: new Map(), + }); + }); + }); + context('App', () => { + it('should return variables correctly', async () => { + await fixture.init( + path.join(DATA_ROOT, 'stack-scratch/simulate-response.json'), + path.join(DATA_ROOT, 'stack-scratch/sources.json'), + ); + + const { client } = fixture; + const PROGRAM = path.join( + DATA_ROOT, + 'stack-scratch/stack-scratch.teal', + ); + + await client.hitBreakpoint( + { program: PROGRAM }, + { path: PROGRAM, line: 3 }, + ); + + await assertVariables(client, { + pc: 6, + stack: [1005], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 12 }); + + await assertVariables(client, { + pc: 18, + stack: [10], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 22 }); + + await assertVariables(client, { + pc: 34, + stack: [10, 0, 0, 0, 0, 0, 0], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 35 }); + + await assertVariables(client, { + pc: 63, + stack: [10, 30, Buffer.from('1!'), Buffer.from('5!')], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 36 }); + + await assertVariables(client, { + pc: 80, + stack: [ + 10, + 30, + Buffer.from('1!'), + Buffer.from('5!'), + 0, + 2, + 1, + 1, + 5, + BigInt('18446744073709551615'), + ], + scratch: new Map(), + }); + + await advanceTo(client, { program: PROGRAM, line: 37 }); + + await assertVariables(client, { + pc: 82, + stack: [10, 30, Buffer.from('1!'), Buffer.from('5!'), 0, 2, 1, 1, 5], + scratch: new Map([[1, BigInt('18446744073709551615')]]), + }); + + await advanceTo(client, { program: PROGRAM, line: 39 }); + + await assertVariables(client, { + pc: 85, + stack: [10, 30, Buffer.from('1!'), Buffer.from('5!'), 0, 2, 1, 1], + scratch: new Map([ + [1, BigInt('18446744073709551615')], + [5, BigInt('18446744073709551615')], + ]), + }); + + await advanceTo(client, { program: PROGRAM, line: 41 }); + + await assertVariables(client, { + pc: 89, + stack: [10, 30, Buffer.from('1!'), Buffer.from('5!'), 0, 2, 1, 1], + scratch: new Map([ + [1, BigInt('18446744073709551615')], + [5, BigInt('18446744073709551615')], + ]), + }); + + await advanceTo(client, { program: PROGRAM, line: 13 }); + + await assertVariables(client, { + pc: 21, + stack: [30], + scratch: new Map([ + [1, BigInt('18446744073709551615')], + [5, BigInt('18446744073709551615')], + ]), + }); + }); + }); + }); + + describe('Global state changes', () => { + it('should return variables correctly', async () => { + await fixture.init( + path.join(DATA_ROOT, 'app-state-changes/global-simulate-response.json'), + path.join(DATA_ROOT, 'app-state-changes/sources.json'), + ); + + const { client } = fixture; + const PROGRAM = path.join( + DATA_ROOT, + 'app-state-changes/state-changes.teal', + ); + + await client.hitBreakpoint( + { program: PROGRAM }, + { path: PROGRAM, line: 3 }, + ); + + await assertVariables(client, { + pc: 6, + stack: [1050], + apps: [ + { + appID: 1050, + globalState: new ByteArrayMap(), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 14 }); + + await assertVariables(client, { + pc: 37, + stack: [ + Buffer.from('8e169311', 'hex'), + Buffer.from('8913c1f8', 'hex'), + Buffer.from('d513c44e', 'hex'), + Buffer.from('8913c1f8', 'hex'), + ], + apps: [ + { + appID: 1050, + globalState: new ByteArrayMap(), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 31 }); + + await assertVariables(client, { + pc: 121, + stack: [Buffer.from('global-int-key'), 0xdeadbeef], + apps: [ + { + appID: 1050, + globalState: new ByteArrayMap(), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 32 }); + + await assertVariables(client, { + pc: 122, + stack: [], + apps: [ + { + appID: 1050, + globalState: new ByteArrayMap([ + [Buffer.from('global-int-key'), 0xdeadbeef], + ]), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 35 }); + + await assertVariables(client, { + pc: 156, + stack: [], + apps: [ + { + appID: 1050, + globalState: new ByteArrayMap([ + [Buffer.from('global-int-key'), 0xdeadbeef], + [Buffer.from('global-bytes-key'), Buffer.from('welt am draht')], + ]), + }, + ], + }); + }); + }); + + describe('Local state changes', () => { + it('should return variables correctly', async () => { + await fixture.init( + path.join(DATA_ROOT, 'app-state-changes/local-simulate-response.json'), + path.join(DATA_ROOT, 'app-state-changes/sources.json'), + ); + + const { client } = fixture; + const PROGRAM = path.join( + DATA_ROOT, + 'app-state-changes/state-changes.teal', + ); + + await client.hitBreakpoint( + { program: PROGRAM }, + { path: PROGRAM, line: 3 }, + ); + + await assertVariables(client, { + pc: 6, + stack: [1054], + apps: [ + { + appID: 1054, + localState: [ + { + account: + 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', + state: new ByteArrayMap(), + }, + ], + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 14 }); + + await assertVariables(client, { + pc: 37, + stack: [ + Buffer.from('8e169311', 'hex'), + Buffer.from('8913c1f8', 'hex'), + Buffer.from('d513c44e', 'hex'), + Buffer.from('8e169311', 'hex'), + ], + apps: [ + { + appID: 1054, + localState: [ + { + account: + 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', + state: new ByteArrayMap(), + }, + ], + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 21 }); + + await assertVariables(client, { + pc: 69, + stack: [ + algosdk.decodeAddress( + 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', + ).publicKey, + Buffer.from('local-int-key'), + 0xcafeb0ba, + ], + apps: [ + { + appID: 1054, + localState: [ + { + account: + 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', + state: new ByteArrayMap(), + }, + ], + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 22 }); + + await assertVariables(client, { + pc: 70, + stack: [], + apps: [ + { + appID: 1054, + localState: [ + { + account: + 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', + state: new ByteArrayMap([ + [Buffer.from('local-int-key'), 0xcafeb0ba], + ]), + }, + ], + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 26 }); + + await assertVariables(client, { + pc: 96, + stack: [], + apps: [ + { + appID: 1054, + localState: [ + { + account: + 'YGOSQB6R5IVQDJHJUHTIZAJNWNIT7VLMWHXFWY2H5HMWPK7QOPXHELNPJ4', + state: new ByteArrayMap([ + [Buffer.from('local-int-key'), 0xcafeb0ba], + [Buffer.from('local-bytes-key'), Buffer.from('xqcL')], + ]), + }, + ], + }, + ], + }); + }); + }); + + describe('Box state changes', () => { + it('should return variables correctly', async () => { + await fixture.init( + path.join(DATA_ROOT, 'app-state-changes/box-simulate-response.json'), + path.join(DATA_ROOT, 'app-state-changes/sources.json'), + ); + + const { client } = fixture; + const PROGRAM = path.join( + DATA_ROOT, + 'app-state-changes/state-changes.teal', + ); + + await client.hitBreakpoint( + { program: PROGRAM }, + { path: PROGRAM, line: 3 }, + ); + + await assertVariables(client, { + pc: 6, + stack: [1058], + apps: [ + { + appID: 1058, + boxState: new ByteArrayMap(), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 14 }); + + await assertVariables(client, { + pc: 37, + stack: [ + Buffer.from('8e169311', 'hex'), + Buffer.from('8913c1f8', 'hex'), + Buffer.from('d513c44e', 'hex'), + Buffer.from('d513c44e', 'hex'), + ], + apps: [ + { + appID: 1058, + boxState: new ByteArrayMap(), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 40 }); + + await assertVariables(client, { + pc: 183, + stack: [Buffer.from('box-key-1'), Buffer.from('box-value-1')], + apps: [ + { + appID: 1058, + boxState: new ByteArrayMap(), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 41 }); + + await assertVariables(client, { + pc: 184, + stack: [], + apps: [ + { + appID: 1058, + boxState: new ByteArrayMap([ + [Buffer.from('box-key-1'), Buffer.from('box-value-1')], + ]), + }, + ], + }); + + await advanceTo(client, { program: PROGRAM, line: 46 }); + + await assertVariables(client, { + pc: 198, + stack: [], + apps: [ + { + appID: 1058, + boxState: new ByteArrayMap([ + [Buffer.from('box-key-1'), Buffer.from('box-value-1')], + [Buffer.from('box-key-2'), Buffer.from('')], + ]), + }, + ], + }); + }); + }); + + describe('Source mapping', () => { + interface SourceInfo { + path: string; + validBreakpoints: DebugProtocol.BreakpointLocation[]; + } + + const testSources: SourceInfo[] = [ + { + path: path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), + validBreakpoints: [ + { line: 4, column: 1 }, + { line: 4, column: 20 }, + { line: 4, column: 27 }, + { line: 4, column: 31 }, + { line: 7, column: 5 }, + { line: 7, column: 12 }, + { line: 7, column: 19 }, + { line: 8, column: 5 }, + { line: 8, column: 12 }, + { line: 8, column: 19 }, + { line: 12, column: 5 }, + { line: 13, column: 5 }, + ], + }, + { + path: path.join(DATA_ROOT, 'sourcemap-test/lib.teal'), + validBreakpoints: [ + { line: 2, column: 22 }, + { line: 2, column: 26 }, + ], + }, + ]; + + it('should return correct breakpoint locations', async () => { + await fixture.init( + path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), + path.join(DATA_ROOT, 'sourcemap-test/sources.json'), + ); + + const { client } = fixture; + + for (const source of testSources) { + const response = await client.breakpointLocationsRequest({ + source: { + path: source.path, + }, + line: 0, + endLine: 100, + }); + assert.ok(response.success); + + const actualBreakpointLocations = response.body.breakpoints.slice(); + // Sort the response so that it's easier to compare + actualBreakpointLocations.sort((a, b) => { + if (a.line === b.line) { + return (a.column ?? 0) - (b.column ?? 0); + } + return a.line - b.line; + }); + + assert.deepStrictEqual( + actualBreakpointLocations, + source.validBreakpoints, + ); + } + }); + + it('should correctly set and stop at valid breakpoints', async () => { + await fixture.init( + path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), + path.join(DATA_ROOT, 'sourcemap-test/sources.json'), + ); + + const { client } = fixture; + + await Promise.all([ + client.configurationSequence(), + client.launch({ + program: path.join( + DATA_ROOT, + 'sourcemap-test/simulate-response.json', + ), + stopOnEntry: true, + }), + client.assertStoppedLocation('entry', {}), + ]); + + for (const source of testSources) { + const result = await client.setBreakpointsRequest({ + source: { path: source.path }, + breakpoints: source.validBreakpoints, + }); + assert.ok(result.success); + + assert.ok(result.body.breakpoints.every((bp) => bp.verified)); + const actualBreakpointLocations = result.body.breakpoints.map((bp) => ({ + line: bp.line, + column: bp.column, + })); + assert.deepStrictEqual( + actualBreakpointLocations, + source.validBreakpoints, + ); + } + + // The breakpoints will not necessarily be hit in order, since PCs map to different + // places in the source file, so we will keep track of which breakpoints have been hit. + const seenBreakpointLocation: boolean[][] = testSources.map((source) => + source.validBreakpoints.map(() => false), + ); + + while ( + seenBreakpointLocation.some((sourceBreakpoints) => + sourceBreakpoints.some((seen) => !seen), + ) + ) { + await client.continueRequest({ threadId: 1 }); + const stoppedResponse = await client.assertStoppedLocation( + 'breakpoint', + {}, + ); + const stoppedFrame = stoppedResponse.body.stackFrames[0]; + let found = false; + for ( + let sourceIndex = 0; + sourceIndex < testSources.length; + sourceIndex++ + ) { + const source = testSources[sourceIndex]; + if (source.path !== stoppedFrame.source?.path) { + continue; + } + for (let i = 0; i < source.validBreakpoints.length; i++) { + if ( + source.validBreakpoints[i].line === stoppedFrame.line && + source.validBreakpoints[i].column === stoppedFrame.column + ) { + assert.strictEqual( + seenBreakpointLocation[sourceIndex][i], + false, + `Breakpoint ${i} was hit twice. Line: ${stoppedFrame.line}, Column: ${stoppedFrame.column}, Path: ${source.path}`, + ); + seenBreakpointLocation[sourceIndex][i] = true; + found = true; + break; + } + } + } + assert.ok( + found, + `Breakpoint at path ${stoppedFrame.source?.path}, line ${stoppedFrame.line}, column ${stoppedFrame.column} was not expected`, + ); + } + }); + + it('should correctly handle invalid breakpoints and not stop at them', async () => { + await fixture.init( + path.join(DATA_ROOT, 'sourcemap-test/simulate-response.json'), + path.join(DATA_ROOT, 'sourcemap-test/sources.json'), + ); + + const { client } = fixture; + + await Promise.all([ + client.configurationSequence(), + client.launch({ + program: path.join( + DATA_ROOT, + 'sourcemap-test/simulate-response.json', + ), + stopOnEntry: true, + }), + client.assertStoppedLocation('entry', {}), + ]); + + const result = await client.setBreakpointsRequest({ + source: { + path: path.join(DATA_ROOT, 'sourcemap-test/sourcemap-test.teal'), + }, + breakpoints: [ + { line: 0, column: 0 }, + { line: 100, column: 0 }, + { line: 0, column: 100 }, + { line: 100, column: 100 }, + ], + }); + assert.ok(result.success); + + assert.ok(result.body.breakpoints.every((bp) => !bp.verified)); + + await Promise.all([ + client.continueRequest({ threadId: 1 }), + client.waitForEvent('terminated'), + ]); + }); + }); }); diff --git a/tests/client.ts b/tests/client.ts index 4e4f0de..cb39856 100644 --- a/tests/client.ts +++ b/tests/client.ts @@ -4,60 +4,141 @@ import { DebugClient as DebugClientBase } from '@vscode/debugadapter-testsupport import { DebugProtocol } from '@vscode/debugprotocol'; export class DebugClient extends DebugClientBase { + private lastStoppedEvent: DebugProtocol.StoppedEvent | undefined; - private lastStoppedEvent: DebugProtocol.StoppedEvent | undefined; + constructor( + debugAdapterRuntime: string, + debugAdapterExecutable: string, + debugType: string, + spawnOptions?: SpawnOptions, + enableStderr?: boolean, + ) { + super( + debugAdapterRuntime, + debugAdapterExecutable, + debugType, + spawnOptions, + enableStderr, + ); - constructor(debugAdapterRuntime: string, debugAdapterExecutable: string, debugType: string, spawnOptions?: SpawnOptions, enableStderr?: boolean) { - super(debugAdapterRuntime, debugAdapterExecutable, debugType, spawnOptions, enableStderr); + this.on('stopped', (event) => { + this.lastStoppedEvent = event; + }); + this.on('continued', () => { + this.lastStoppedEvent = undefined; + }); + } - this.on('stopped', event => { - this.lastStoppedEvent = event; - }); - this.on('continued', () => { - this.lastStoppedEvent = undefined; - }); - } + continueRequest( + args: DebugProtocol.ContinueArguments, + ): Promise { + // Optimistically clear the last stopped event. It's important to do this before we send the + // request, otherwise we might miss a stopped event that happens immediately after. + this.lastStoppedEvent = undefined; + return super.continueRequest(args); + } - async continueRequest(args: DebugProtocol.ContinueArguments): Promise { - // Optimistically clear the last stopped event. It's important to do this before we send the - // continue request, otherwise we might miss a stopped event that happens immediately after. - this.lastStoppedEvent = undefined; - const response = await super.continueRequest(args); - return response; - } + nextRequest( + args: DebugProtocol.NextArguments, + ): Promise { + // Optimistically clear the last stopped event. It's important to do this before we send the + // request, otherwise we might miss a stopped event that happens immediately after. + this.lastStoppedEvent = undefined; + return super.nextRequest(args); + } - async waitForStop(): Promise { - if (typeof this.lastStoppedEvent !== 'undefined') { - return Promise.resolve(this.lastStoppedEvent); - } - const event = (await this.waitForEvent('stopped')) as DebugProtocol.StoppedEvent; - return event; - } + stepInRequest( + args: DebugProtocol.StepInArguments, + ): Promise { + // Optimistically clear the last stopped event. It's important to do this before we send the + // request, otherwise we might miss a stopped event that happens immediately after. + this.lastStoppedEvent = undefined; + return super.stepInRequest(args); + } + + stepOutRequest( + args: DebugProtocol.StepOutArguments, + ): Promise { + // Optimistically clear the last stopped event. It's important to do this before we send the + // request, otherwise we might miss a stopped event that happens immediately after. + this.lastStoppedEvent = undefined; + return super.stepOutRequest(args); + } + + stepBackRequest( + args: DebugProtocol.StepBackArguments, + ): Promise { + // Optimistically clear the last stopped event. It's important to do this before we send the + // request, otherwise we might miss a stopped event that happens immediately after. + this.lastStoppedEvent = undefined; + return super.stepBackRequest(args); + } + + reverseContinueRequest( + args: DebugProtocol.ReverseContinueArguments, + ): Promise { + // Optimistically clear the last stopped event. It's important to do this before we send the + // request, otherwise we might miss a stopped event that happens immediately after. + this.lastStoppedEvent = undefined; + return super.reverseContinueRequest(args); + } - async assertStoppedLocation(reason: string, expected: { - path?: string | RegExp; - line?: number; - column?: number; - }): Promise { - const stoppedEvent = await this.waitForStop(); - assert.strictEqual(stoppedEvent.body.reason, reason); - - const stackTraceResponse = await this.stackTraceRequest({ threadId: stoppedEvent.body.threadId! }); - - const frame = stackTraceResponse.body.stackFrames[0]; - if (typeof expected.path === 'string' || expected.path instanceof RegExp) { - this.assertPath(frame.source?.path!, expected.path, `stopped location: path mismatch: ${frame.source?.path} vs ${expected.path}`); - } - if (typeof expected.line === 'number') { - assert.strictEqual(frame.line, expected.line, `stopped location: line mismatch: ${frame.line} vs ${expected.line}`); - } - if (typeof expected.column === 'number') { - assert.strictEqual(frame.column, expected.column, `stopped location: column mismatch: ${frame.column} vs ${expected.column}`); - } - return stackTraceResponse; + async waitForStop(): Promise { + if (typeof this.lastStoppedEvent !== 'undefined') { + return Promise.resolve(this.lastStoppedEvent); } + const event = (await this.waitForEvent( + 'stopped', + )) as DebugProtocol.StoppedEvent; + return event; + } - breakpointLocationsRequest(args: DebugProtocol.BreakpointLocationsArguments): Promise { - return this.send('breakpointLocations', args) as Promise; + async assertStoppedLocation( + reason: string, + expected: { + path?: string | RegExp; + line?: number; + column?: number; + }, + ): Promise { + const stoppedEvent = await this.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, reason); + + const stackTraceResponse = await this.stackTraceRequest({ + threadId: stoppedEvent.body.threadId!, + }); + + const frame = stackTraceResponse.body.stackFrames[0]; + if (typeof expected.path === 'string' || expected.path instanceof RegExp) { + this.assertPath( + frame.source?.path!, + expected.path, + `stopped location: path mismatch: ${frame.source?.path} vs ${expected.path}`, + ); + } + if (typeof expected.line === 'number') { + assert.strictEqual( + frame.line, + expected.line, + `stopped location: line mismatch: ${frame.line} vs ${expected.line}`, + ); } + if (typeof expected.column === 'number') { + assert.strictEqual( + frame.column, + expected.column, + `stopped location: column mismatch: ${frame.column} vs ${expected.column}`, + ); + } + return stackTraceResponse; + } + + breakpointLocationsRequest( + args: DebugProtocol.BreakpointLocationsArguments, + ): Promise { + return this.send( + 'breakpointLocations', + args, + ) as Promise; + } } diff --git a/tests/testing.ts b/tests/testing.ts index 5e0a97b..0cf47d4 100644 --- a/tests/testing.ts +++ b/tests/testing.ts @@ -2,263 +2,442 @@ import * as assert from 'assert'; import * as path from 'path'; import * as fs from 'fs/promises'; import { DebugClient } from './client'; -import { FileAccessor, ByteArrayMap } from '../src/debugAdapter/utils'; +import { BasicServer } from '../src/debugAdapter/basicServer'; +import { + FileAccessor, + ByteArrayMap, + TEALDebuggingAssets, +} from '../src/debugAdapter/utils'; export const PROJECT_ROOT = path.join(__dirname, '../'); -export const DEBUG_CLIENT_PATH = path.join(PROJECT_ROOT, 'out/debugAdapter/debugAdapter.js'); +export const DEBUG_CLIENT_PATH = path.join( + PROJECT_ROOT, + 'out/debugAdapter/debugAdapter.js', +); export const DATA_ROOT = path.join(PROJECT_ROOT, 'sampleWorkspace/'); export const testFileAccessor: FileAccessor = { - isWindows: typeof process !== 'undefined' && process.platform === 'win32', - async readFile(path: string): Promise { - return await fs.readFile(path); - }, - async writeFile(path: string, contents: Uint8Array) { - return await fs.writeFile(path, contents); - } + isWindows: typeof process !== 'undefined' && process.platform === 'win32', + async readFile(path: string): Promise { + return await fs.readFile(path); + }, + async writeFile(path: string, contents: Uint8Array) { + return await fs.writeFile(path, contents); + }, }; -export function assertAvmValuesEqual(actual: { value: string, type?: string }, expectedValue: number | bigint | Uint8Array) { - if (expectedValue instanceof Uint8Array) { - assert.strictEqual(actual.type, 'byte[]'); - assert.ok(actual.value.startsWith('0x')); - const actualBytes = Buffer.from(actual.value.slice(2), 'hex'); - assert.deepStrictEqual(new Uint8Array(actualBytes), new Uint8Array(expectedValue)); - } else if (typeof expectedValue === 'number' || typeof expectedValue === 'bigint') { - assert.strictEqual(actual.type, 'uint64'); - assert.strictEqual(BigInt(actual.value), BigInt(expectedValue)); - } else { - throw new Error(`Improper expected value: ${expectedValue}`); - } +export class TestFixture { + private _client: DebugClient | undefined; + private _server: BasicServer | undefined; + + public get client(): DebugClient { + if (!this._client) { + throw new Error('Not initialized'); + } + return this._client; + } + + private get server(): BasicServer { + if (!this._server) { + throw new Error('Not initialized'); + } + return this._server; + } + + public async init( + simulateResponsePath: string, + txnGroupSourcesDescriptionPath: string, + ) { + const debugAssets: TEALDebuggingAssets = + await TEALDebuggingAssets.loadFromFiles( + testFileAccessor, + simulateResponsePath, + txnGroupSourcesDescriptionPath, + ); + this._server = new BasicServer(testFileAccessor, debugAssets); + + this._client = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal'); + await this._client.start(this._server.port()); + + // this._client = new DebugClient('node', DEBUG_CLIENT_PATH, 'teal', { + // env: { + // ...process.env, + // /* eslint-disable @typescript-eslint/naming-convention */ + // ALGORAND_SIMULATION_RESPONSE_PATH: simulateResponsePath, + // ALGORAND_TXN_GROUP_SOURCES_DESCRIPTION_PATH: txnGroupSourcesDescriptionPath, + // /* eslint-enable @typescript-eslint/naming-convention */ + // } + // }, true); + // await this._client.start(); + } + + public async reset() { + await this.client.stop(); + this.server.dispose(); + this._client = undefined; + this._server = undefined; + } } -export async function assertVariables(dc: DebugClient, { - pc, - stack, - scratch, - apps, -}: { - pc?: number, - stack?: Array, - scratch?: Map, - apps?: Array<{ - appID: number, - globalState?: ByteArrayMap, - localState?: Array<{ - account: string, - state: ByteArrayMap, - }>, - boxState?: ByteArrayMap, - }>, -}) { - const stackResponse = await dc.stackTraceRequest({ threadId: 1 }); - assert.ok(stackResponse.success); - const latestFrame = stackResponse.body.stackFrames[0].id; - - const scopesResponse = await dc.scopesRequest({ frameId: latestFrame }); - assert.ok(scopesResponse.success); - const scopes = scopesResponse.body.scopes; - - const executionScope = scopes.find(scope => scope.name.startsWith('Program State')); - assert.ok(executionScope); - - const executionScopeResponse = await dc.variablesRequest({ variablesReference: executionScope.variablesReference }); - assert.ok(executionScopeResponse.success); - const executionScopeVariables = executionScopeResponse.body.variables; - - const onChainScope = scopes.find(scope => scope.name === 'On-chain State'); - assert.ok(onChainScope); - - const onChainScopeResponse = await dc.variablesRequest({ variablesReference: onChainScope.variablesReference }); - assert.ok(onChainScopeResponse.success); - const onChainScopeVariables = onChainScopeResponse.body.variables; - - const appStateVariable = onChainScopeVariables.find(variable => variable.name === 'app'); - assert.ok(appStateVariable); - - const appStateVariableResponse = await dc.variablesRequest({ variablesReference: appStateVariable.variablesReference }); - assert.ok(appStateVariableResponse.success); - const appStates = appStateVariableResponse.body.variables; - - if (typeof pc !== 'undefined') { - const pcVariable = executionScopeVariables.find(variable => variable.name === 'pc'); - assert.ok(pcVariable); - assert.strictEqual(pcVariable.type, 'uint64'); - assert.strictEqual(pcVariable.value, pc.toString()); - - await assertEvaluationEquals(dc, latestFrame, 'pc', { value: pc.toString(), type: 'uint64' }); - } - - if (typeof stack !== 'undefined') { - const stackParentVariable = executionScopeVariables.find(variable => variable.name === 'stack'); - assert.ok(stackParentVariable); - - const stackVariableResponse = await dc.variablesRequest({ variablesReference: stackParentVariable.variablesReference }); - assert.ok(stackVariableResponse.success); - const stackVariables = stackVariableResponse.body.variables; - - assert.strictEqual(stackVariables.length, stack.length); - - for (let i = 0; i < stack.length; i++) { - assert.strictEqual(stackVariables[i].name, i.toString()); - assertAvmValuesEqual(stackVariables[i], stack[i]); - } - - await Promise.all(stack.map(async (expectedValue, i) => { - if (expectedValue instanceof Uint8Array) { - await assertEvaluationEquals(dc, latestFrame, `stack[${i}]`, { value: '0x' + Buffer.from(expectedValue).toString('hex'), type: 'byte[]' }); - } else if (typeof expectedValue === 'number' || typeof expectedValue === 'bigint') { - await assertEvaluationEquals(dc, latestFrame, `stack[${i}]`, { value: expectedValue.toString(), type: 'uint64' }); - } else { - throw new Error(`Improper expected stack value: ${expectedValue}`); - } - })); - } - - if (typeof scratch !== 'undefined') { - for (const key of scratch.keys()) { - if (key < 0 || key > 255) { - assert.fail(`Invalid scratch key: ${key}`); - } - } - - const scratchParentVariable = executionScopeVariables.find(variable => variable.name === 'scratch'); - assert.ok(scratchParentVariable); - - const scratchVariableResponse = await dc.variablesRequest({ variablesReference: scratchParentVariable.variablesReference }); - assert.ok(scratchVariableResponse.success); - const scratchVariables = scratchVariableResponse.body.variables; - - assert.strictEqual(scratchVariables.length, 256); - - for (let i = 0; i < 256; i++) { - assert.strictEqual(scratchVariables[i].name, i.toString()); - let expectedValue = scratch.get(i); - if (typeof expectedValue === 'undefined') { - expectedValue = 0; - } - assertAvmValuesEqual(scratchVariables[i], expectedValue); - } - - await Promise.all(scratchVariables.map(async (actual, i) => { - let expectedValue = scratch.get(i); - if (typeof expectedValue === 'undefined') { - expectedValue = 0; - } - - if (expectedValue instanceof Uint8Array) { - await assertEvaluationEquals(dc, latestFrame, `scratch[${i}]`, { value: '0x' + Buffer.from(expectedValue).toString('hex'), type: 'byte[]' }); - } else if (typeof expectedValue === 'number' || typeof expectedValue === 'bigint') { - await assertEvaluationEquals(dc, latestFrame, `scratch[${i}]`, { value: expectedValue.toString(), type: 'uint64' }); - } else { - throw new Error(`Improper expected scratch value: ${expectedValue}`); - } - })); - } - - if (typeof apps !== 'undefined') { - for (const expectedAppState of apps) { - const { appID, globalState, localState, boxState } = expectedAppState; - const appState = appStates.find(variable => variable.name === appID.toString()); - assert.ok(appState, `Expected app state for app ID ${appID} not found`); - - const appStateResponse = await dc.variablesRequest({ variablesReference: appState.variablesReference }); - assert.ok(appStateResponse.success); - const appStateVariables = appStateResponse.body.variables; - - if (typeof globalState !== 'undefined') { - const globalStateVariable = appStateVariables.find(variable => variable.name === 'global'); - assert.ok(globalStateVariable); - - const globalStateResponse = await dc.variablesRequest({ variablesReference: globalStateVariable.variablesReference }); - assert.ok(globalStateResponse.success); - const globalStateVariables = globalStateResponse.body.variables; - - for (const [key, expectedValue] of globalState.entries()) { - const keyStr = '0x' + Buffer.from(key).toString('hex'); - const actual = globalStateVariables.find(variable => variable.name === keyStr); - assert.ok(actual, `Expected global state key "${keyStr}" not found`); - assertAvmValuesEqual(actual, expectedValue); - } - - assert.strictEqual(globalStateVariables.length, globalState.size); - } - - if (typeof localState !== 'undefined') { - const localStateVariable = appStateVariables.find(variable => variable.name === 'local'); - assert.ok(localStateVariable); - - const localStateResponse = await dc.variablesRequest({ variablesReference: localStateVariable.variablesReference }); - assert.ok(localStateResponse.success); - const localStateAccounts = localStateResponse.body.variables; - - for (const expectedAccountState of localState) { - const accountLocalState = localStateAccounts.find(variable => variable.name === expectedAccountState.account); - assert.ok(accountLocalState, `Expected local state for account ${expectedAccountState.account} not found`); - - const accountLocalStateResponse = await dc.variablesRequest({ variablesReference: accountLocalState.variablesReference }); - assert.ok(accountLocalStateResponse.success); - const accountLocalStateVariables = accountLocalStateResponse.body.variables; - - for (const [key, expectedValue] of expectedAccountState.state.entries()) { - const keyStr = '0x' + Buffer.from(key).toString('hex'); - const actual = accountLocalStateVariables.find(variable => variable.name === keyStr); - assert.ok(actual, `Expected local state key "${keyStr}" not found`); - assertAvmValuesEqual(actual, expectedValue); - } - - assert.strictEqual(accountLocalStateVariables.length, expectedAccountState.state.size); - } - - assert.strictEqual(localStateAccounts.length, localState.length); - } - - if (typeof boxState !== 'undefined') { - const boxStateVariable = appStateVariables.find(variable => variable.name === 'box'); - assert.ok(boxStateVariable); - - const boxStateResponse = await dc.variablesRequest({ variablesReference: boxStateVariable.variablesReference }); - assert.ok(boxStateResponse.success); - const boxStateVariables = boxStateResponse.body.variables; - - for (const [key, expectedValue] of boxState.entries()) { - const keyStr = '0x' + Buffer.from(key).toString('hex'); - const actual = boxStateVariables.find(variable => variable.name === keyStr); - assert.ok(actual, `Expected box state key "${keyStr}" not found`); - assertAvmValuesEqual(actual, expectedValue); - } - - assert.strictEqual(boxStateVariables.length, boxState.size); - } - } - } +export function assertAvmValuesEqual( + actual: { value: string; type?: string }, + expectedValue: number | bigint | Uint8Array, +) { + if (expectedValue instanceof Uint8Array) { + assert.strictEqual(actual.type, 'byte[]'); + assert.ok(actual.value.startsWith('0x')); + const actualBytes = Buffer.from(actual.value.slice(2), 'hex'); + assert.deepStrictEqual( + new Uint8Array(actualBytes), + new Uint8Array(expectedValue), + ); + } else if ( + typeof expectedValue === 'number' || + typeof expectedValue === 'bigint' + ) { + assert.strictEqual(actual.type, 'uint64'); + assert.strictEqual(BigInt(actual.value), BigInt(expectedValue)); + } else { + throw new Error(`Improper expected value: ${expectedValue}`); + } } -export async function advanceTo(dc: DebugClient, args: { program: string, line: number, column?: number} ) { - const breakpointResponse = await dc.setBreakpointsRequest({ - source: { path: args.program }, - breakpoints: [{ - line: args.line, - column: args.column - }], - }); - - assert.ok(breakpointResponse.success); - assert.strictEqual(breakpointResponse.body.breakpoints.length, 1); - const bp = breakpointResponse.body.breakpoints[0]; - assert.ok(bp.verified); - - const continueResponse = await dc.continueRequest({ threadId: 0 }); - assert.ok(continueResponse.success); +export async function assertVariables( + dc: DebugClient, + { + pc, + stack, + scratch, + apps, + }: { + pc?: number; + stack?: Array; + scratch?: Map; + apps?: Array<{ + appID: number; + globalState?: ByteArrayMap; + localState?: Array<{ + account: string; + state: ByteArrayMap; + }>; + boxState?: ByteArrayMap; + }>; + }, + frameId?: number, +) { + if (typeof frameId === 'undefined') { + const stackResponse = await dc.stackTraceRequest({ threadId: 1 }); + assert.ok(stackResponse.success); + frameId = stackResponse.body.stackFrames[0].id; + } + + const scopesResponse = await dc.scopesRequest({ frameId }); + assert.ok(scopesResponse.success); + const scopes = scopesResponse.body.scopes; + + const executionScope = scopes.find((scope) => + scope.name.startsWith('Program State'), + ); + assert.ok(executionScope); + + const executionScopeResponse = await dc.variablesRequest({ + variablesReference: executionScope.variablesReference, + }); + assert.ok(executionScopeResponse.success); + const executionScopeVariables = executionScopeResponse.body.variables; + + const onChainScope = scopes.find((scope) => scope.name === 'On-chain State'); + assert.ok(onChainScope); + + const onChainScopeResponse = await dc.variablesRequest({ + variablesReference: onChainScope.variablesReference, + }); + assert.ok(onChainScopeResponse.success); + const onChainScopeVariables = onChainScopeResponse.body.variables; + + const appStateVariable = onChainScopeVariables.find( + (variable) => variable.name === 'app', + ); + assert.ok(appStateVariable); + + const appStateVariableResponse = await dc.variablesRequest({ + variablesReference: appStateVariable.variablesReference, + }); + assert.ok(appStateVariableResponse.success); + const appStates = appStateVariableResponse.body.variables; + + if (typeof pc !== 'undefined') { + const pcVariable = executionScopeVariables.find( + (variable) => variable.name === 'pc', + ); + assert.ok(pcVariable); + assert.strictEqual(pcVariable.type, 'uint64'); + assert.strictEqual(pcVariable.value, pc.toString()); + + await assertEvaluationEquals(dc, frameId, 'pc', { + value: pc.toString(), + type: 'uint64', + }); + } + + if (typeof stack !== 'undefined') { + const stackParentVariable = executionScopeVariables.find( + (variable) => variable.name === 'stack', + ); + assert.ok(stackParentVariable); + + const stackVariableResponse = await dc.variablesRequest({ + variablesReference: stackParentVariable.variablesReference, + }); + assert.ok(stackVariableResponse.success); + const stackVariables = stackVariableResponse.body.variables; + + assert.strictEqual(stackVariables.length, stack.length); + + for (let i = 0; i < stack.length; i++) { + assert.strictEqual(stackVariables[i].name, i.toString()); + assertAvmValuesEqual(stackVariables[i], stack[i]); + } + + await Promise.all( + stack.map(async (expectedValue, i) => { + if (expectedValue instanceof Uint8Array) { + await assertEvaluationEquals(dc, frameId!, `stack[${i}]`, { + value: '0x' + Buffer.from(expectedValue).toString('hex'), + type: 'byte[]', + }); + } else if ( + typeof expectedValue === 'number' || + typeof expectedValue === 'bigint' + ) { + await assertEvaluationEquals(dc, frameId!, `stack[${i}]`, { + value: expectedValue.toString(), + type: 'uint64', + }); + } else { + throw new Error(`Improper expected stack value: ${expectedValue}`); + } + }), + ); + } + + if (typeof scratch !== 'undefined') { + for (const key of scratch.keys()) { + if (key < 0 || key > 255) { + assert.fail(`Invalid scratch key: ${key}`); + } + } + + const scratchParentVariable = executionScopeVariables.find( + (variable) => variable.name === 'scratch', + ); + assert.ok(scratchParentVariable); + + const scratchVariableResponse = await dc.variablesRequest({ + variablesReference: scratchParentVariable.variablesReference, + }); + assert.ok(scratchVariableResponse.success); + const scratchVariables = scratchVariableResponse.body.variables; + + assert.strictEqual(scratchVariables.length, 256); + + for (let i = 0; i < 256; i++) { + assert.strictEqual(scratchVariables[i].name, i.toString()); + let expectedValue = scratch.get(i); + if (typeof expectedValue === 'undefined') { + expectedValue = 0; + } + assertAvmValuesEqual(scratchVariables[i], expectedValue); + } + + await Promise.all( + scratchVariables.map(async (actual, i) => { + let expectedValue = scratch.get(i); + if (typeof expectedValue === 'undefined') { + expectedValue = 0; + } + + if (expectedValue instanceof Uint8Array) { + await assertEvaluationEquals(dc, frameId!, `scratch[${i}]`, { + value: '0x' + Buffer.from(expectedValue).toString('hex'), + type: 'byte[]', + }); + } else if ( + typeof expectedValue === 'number' || + typeof expectedValue === 'bigint' + ) { + await assertEvaluationEquals(dc, frameId!, `scratch[${i}]`, { + value: expectedValue.toString(), + type: 'uint64', + }); + } else { + throw new Error(`Improper expected scratch value: ${expectedValue}`); + } + }), + ); + } + + if (typeof apps !== 'undefined') { + for (const expectedAppState of apps) { + const { appID, globalState, localState, boxState } = expectedAppState; + const appState = appStates.find( + (variable) => variable.name === appID.toString(), + ); + assert.ok(appState, `Expected app state for app ID ${appID} not found`); + + const appStateResponse = await dc.variablesRequest({ + variablesReference: appState.variablesReference, + }); + assert.ok(appStateResponse.success); + const appStateVariables = appStateResponse.body.variables; + + if (typeof globalState !== 'undefined') { + const globalStateVariable = appStateVariables.find( + (variable) => variable.name === 'global', + ); + assert.ok(globalStateVariable); + + const globalStateResponse = await dc.variablesRequest({ + variablesReference: globalStateVariable.variablesReference, + }); + assert.ok(globalStateResponse.success); + const globalStateVariables = globalStateResponse.body.variables; + + for (const [key, expectedValue] of globalState.entries()) { + const keyStr = '0x' + Buffer.from(key).toString('hex'); + const actual = globalStateVariables.find( + (variable) => variable.name === keyStr, + ); + assert.ok(actual, `Expected global state key "${keyStr}" not found`); + assertAvmValuesEqual(actual, expectedValue); + } + + assert.strictEqual(globalStateVariables.length, globalState.size); + } + + if (typeof localState !== 'undefined') { + const localStateVariable = appStateVariables.find( + (variable) => variable.name === 'local', + ); + assert.ok(localStateVariable); + + const localStateResponse = await dc.variablesRequest({ + variablesReference: localStateVariable.variablesReference, + }); + assert.ok(localStateResponse.success); + const localStateAccounts = localStateResponse.body.variables; + + for (const expectedAccountState of localState) { + const accountLocalState = localStateAccounts.find( + (variable) => variable.name === expectedAccountState.account, + ); + assert.ok( + accountLocalState, + `Expected local state for account ${expectedAccountState.account} not found`, + ); + + const accountLocalStateResponse = await dc.variablesRequest({ + variablesReference: accountLocalState.variablesReference, + }); + assert.ok(accountLocalStateResponse.success); + const accountLocalStateVariables = + accountLocalStateResponse.body.variables; + + for (const [ + key, + expectedValue, + ] of expectedAccountState.state.entries()) { + const keyStr = '0x' + Buffer.from(key).toString('hex'); + const actual = accountLocalStateVariables.find( + (variable) => variable.name === keyStr, + ); + assert.ok(actual, `Expected local state key "${keyStr}" not found`); + assertAvmValuesEqual(actual, expectedValue); + } + + assert.strictEqual( + accountLocalStateVariables.length, + expectedAccountState.state.size, + ); + } + + assert.strictEqual(localStateAccounts.length, localState.length); + } + + if (typeof boxState !== 'undefined') { + const boxStateVariable = appStateVariables.find( + (variable) => variable.name === 'box', + ); + assert.ok(boxStateVariable); + + const boxStateResponse = await dc.variablesRequest({ + variablesReference: boxStateVariable.variablesReference, + }); + assert.ok(boxStateResponse.success); + const boxStateVariables = boxStateResponse.body.variables; + + for (const [key, expectedValue] of boxState.entries()) { + const keyStr = '0x' + Buffer.from(key).toString('hex'); + const actual = boxStateVariables.find( + (variable) => variable.name === keyStr, + ); + assert.ok(actual, `Expected box state key "${keyStr}" not found`); + assertAvmValuesEqual(actual, expectedValue); + } + + assert.strictEqual(boxStateVariables.length, boxState.size); + } + } + } +} - await dc.assertStoppedLocation('breakpoint', { path: args.program, line: args.line, column: args.column }); +export async function advanceTo( + dc: DebugClient, + args: { program: string; line: number; column?: number }, +) { + const breakpointResponse = await dc.setBreakpointsRequest({ + source: { path: args.program }, + breakpoints: [ + { + line: args.line, + column: args.column, + }, + ], + }); + + assert.ok(breakpointResponse.success); + assert.strictEqual(breakpointResponse.body.breakpoints.length, 1); + const bp = breakpointResponse.body.breakpoints[0]; + assert.ok(bp.verified); + + const continueResponse = await dc.continueRequest({ threadId: 0 }); + assert.ok(continueResponse.success); + + await dc.assertStoppedLocation('breakpoint', { + path: args.program, + line: args.line, + column: args.column, + }); } -export async function assertEvaluationEquals(dc: DebugClient, frameId: number, expression: string, expected: { value: string, type?: string }) { - const response = await dc.evaluateRequest({ expression, frameId }); - assert.ok(response.success); - assert.strictEqual(response.body.result, expected.value, `Expected "${expression}" to evaluate to "${expected.value}", but got "${response.body.result}"`); - if (expected.type) { - assert.strictEqual(response.body.type, expected.type, `Expected "${expression}" to have type "${expected.type}", but got "${response.body.type}"`); - } +export async function assertEvaluationEquals( + dc: DebugClient, + frameId: number, + expression: string, + expected: { value: string; type?: string }, +) { + const response = await dc.evaluateRequest({ expression, frameId }); + assert.ok(response.success); + assert.strictEqual( + response.body.result, + expected.value, + `Expected "${expression}" to evaluate to "${expected.value}", but got "${response.body.result}"`, + ); + if (expected.type) { + assert.strictEqual( + response.body.type, + expected.type, + `Expected "${expression}" to have type "${expected.type}", but got "${response.body.type}"`, + ); + } } diff --git a/tsconfig.json b/tsconfig.json index 12ee259..fc27dad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,31 +1,25 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "ES2020", - "outDir": "out", - "lib": [ - "WebWorker", - "ES2020" - ], - "sourceMap": true, - "rootDir": ".", - "strict": true, /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["WebWorker", "ES2020"], + "sourceMap": true, + "rootDir": ".", + "strict": true /* enable all strict type-checking options */, + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ - "noImplicitAny": false, - "removeComments": false, - "noUnusedLocals": true, - "noImplicitThis": true, - "inlineSourceMap": false, - "preserveConstEnums": true, - "strictNullChecks": true, - "noUnusedParameters": false - }, - "include": [ - "src/**/*", - "test/**/*" - ], + "noImplicitAny": false, + "removeComments": false, + "noUnusedLocals": true, + "noImplicitThis": true, + "inlineSourceMap": false, + "preserveConstEnums": true, + "strictNullChecks": true, + "noUnusedParameters": false + }, + "include": ["src/**/*", "test/**/*"] }