diff --git a/assets/panel/prefs.js b/assets/panel/prefs.js index 46bc9771f3..1720170293 100644 --- a/assets/panel/prefs.js +++ b/assets/panel/prefs.js @@ -44,3 +44,4 @@ pref("devtools.debugger.features.shortcuts", true); pref("devtools.debugger.project-directory-root", ""); pref("devtools.debugger.features.root", false); pref("devtools.debugger.features.column-breakpoints", false); +pref("devtools.debugger.features.map-scopes", true); diff --git a/flow-typed/debugger-html.js b/flow-typed/debugger-html.js index d389c036ef..dfb8f3701d 100644 --- a/flow-typed/debugger-html.js +++ b/flow-typed/debugger-html.js @@ -323,6 +323,11 @@ declare module "debugger-html" { location: Location, parameterNames: string[] }, + syntheticScopes?: { + scopes: SyntheticScope[], + groupIndex: number, + groupLength: number + }, type: string }; } diff --git a/package.json b/package.json index f57c6dc1f6..18d73ce45b 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,19 @@ "start": "node bin/dev-server", "start-app": "TARGET=application node bin/dev-server", "flow": "flow", - "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check", + "eslint-check": + "eslint --print-config .eslintrc.js | eslint-config-prettier-check", "prettier": "node bin/prettier.js", "license-check": "devtools-license-check", "links": "ls -l node_modules/ | grep ^l || echo 'no linked packages'", "lint": "run-p lint-css lint-js lint-md", "lint-css": "stylelint \"src/components/**/*.css\"", "lint-js": "eslint *.js \"src/**/*.js\" --fix", - "lint-md": "remark -u devtools-linters/markdown/preset -qf *.md src configs docs", + "lint-md": + "remark -u devtools-linters/markdown/preset -qf *.md src configs docs", "lint-fix": "yarn lint-js -- --fix", - "mochi": "mochii --mc ./firefox --interactive --default-test-path devtools/client/debugger/new", + "mochi": + "mochii --mc ./firefox --interactive --default-test-path devtools/client/debugger/new", "mochid": "yarn mochi -- --jsdebugger --", "mochir": "yarn mochi -- --repeat 10 --", "mochih": "yarn mochi -- --setenv MOZ_HEADLESS=1 --", @@ -35,14 +38,20 @@ "test:watch": "jest --watch", "test-coverage": "yarn test -- --coverage", "test-all": "yarn test; yarn lint; yarn flow", - "firefox": "start-firefox --start --location https://devtools-html.github.io/debugger-examples/", - "chrome": "start-chrome --location https://devtools-html.github.io/debugger-examples/", + "firefox": + "start-firefox --start --location https://devtools-html.github.io/debugger-examples/", + "chrome": + "start-chrome --location https://devtools-html.github.io/debugger-examples/", "copy-assets": "node bin/copy-assets --symlink", "copy-assets-watch": "node bin/copy-assets --watch --symlink", - "build-docs": "documentation build --format html --sort-order alpha --shallow --document-exported --output docs/reference/ src/types.js src/utils/ src/reducers/ src/actions/ src/test/mochitest/head.js", - "flow-coverage": "flow-coverage-report --threshold 50 -i 'src/actions/*.js' -i 'src/reducers/*.js' -i 'src/utils/*.js' -i 'src/components/*.js' -i 'src/components/**/*.js' -t html -t text", - "flow-utils": "flow-coverage-report -i 'src/utils/*.js' -i 'src/utils/**/*.js' -t text", - "flow-redux": "flow-coverage-report -i 'src/reducers/*.js' -i 'src/actions/*.js' -t text", + "build-docs": + "documentation build --format html --sort-order alpha --shallow --document-exported --output docs/reference/ src/types.js src/utils/ src/reducers/ src/actions/ src/test/mochitest/head.js", + "flow-coverage": + "flow-coverage-report --threshold 50 -i 'src/actions/*.js' -i 'src/reducers/*.js' -i 'src/utils/*.js' -i 'src/components/*.js' -i 'src/components/**/*.js' -t html -t text", + "flow-utils": + "flow-coverage-report -i 'src/utils/*.js' -i 'src/utils/**/*.js' -t text", + "flow-redux": + "flow-coverage-report -i 'src/reducers/*.js' -i 'src/actions/*.js' -t text", "flow-react": "flow-coverage-report -i 'src/components/**/*.js' -t text", "storybook": "start-storybook -p 6006", "snapshot": "NODE_ENV='development' build-storybook && percy-storybook", @@ -68,7 +77,7 @@ "devtools-map-bindings": "^0.3.2", "devtools-reps": "^0.12.4", "devtools-source-editor": "^0.0.8", - "devtools-source-map": "^0.14.5", + "devtools-source-map": "^0.14.7", "devtools-splitter": "^0.0.5", "devtools-utils": "^0.0.10", "fuzzaldrin-plus": "^0.4.1", @@ -90,18 +99,9 @@ "svg-inline-react": "^1.0.2", "wasmparser": "^0.4.10" }, - "files": [ - "src", - "assets" - ], + "files": ["src", "assets"], "greenkeeper": { - "ignore": [ - "react", - "react-dom", - "react-redux", - "redux", - "codemirror" - ] + "ignore": ["react", "react-dom", "react-redux", "redux", "codemirror"] }, "main": "src/main.js", "author": "Jason Laster ", @@ -165,44 +165,18 @@ "workerjs": "github:jasonLaster/workerjs" }, "lint-staged": { - "*.js": [ - "prettier", - "git add" - ], - "src/*.js": [ - "prettier", - "git add" - ], - "src/*/*.js": [ - "prettier", - "git add" - ], - "src/*/!(mochitest)**/*.js": [ - "prettier", - "git add" - ], - "src/*/!(mochitest)*/**/*.js": [ - "prettier", - "git add" - ] + "*.js": ["prettier", "git add"], + "src/*.js": ["prettier", "git add"], + "src/*/*.js": ["prettier", "git add"], + "src/*/!(mochitest)**/*.js": ["prettier", "git add"], + "src/*/!(mochitest)*/**/*.js": ["prettier", "git add"] }, "jest": { "rootDir": "src", - "testMatch": [ - "**/tests/**/*.js" - ], - "testPathIgnorePatterns": [ - "/node_modules/", - "/helpers/", - "/fixtures/" - ], - "transformIgnorePatterns": [ - "node_modules/(?!devtools-)" - ], - "setupFiles": [ - "/test/tests-setup.js", - "jest-localstorage-mock" - ], + "testMatch": ["**/tests/**/*.js"], + "testPathIgnorePatterns": ["/node_modules/", "/helpers/", "/fixtures/"], + "transformIgnorePatterns": ["node_modules/(?!devtools-)"], + "setupFiles": ["/test/tests-setup.js", "jest-localstorage-mock"], "snapshotSerializers": [ "jest-serializer-babel-ast", "enzyme-to-json/serializer" diff --git a/src/actions/pause/fetchScopes.js b/src/actions/pause/fetchScopes.js index ca97f27b8c..eabbc5e235 100644 --- a/src/actions/pause/fetchScopes.js +++ b/src/actions/pause/fetchScopes.js @@ -6,9 +6,37 @@ import { getSource, getSelectedFrame, getFrameScope } from "../../selectors"; import { updateScopeBindings } from "../../utils/pause"; +import { features } from "../../utils/prefs"; import { isGeneratedId } from "devtools-source-map"; +import { loadSourceText } from "../sources/loadSourceText"; import type { ThunkArgs } from "../types"; +import type { Scope, Frame } from "debugger-html"; + +function mapScopes(scopes: Scope, frame: Frame) { + return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) { + const mappedScopes = await updateScopeBindings( + scopes, + frame.generatedLocation, + frame.location, + { + async getLocationScopes(location, astScopes) { + return sourceMaps.getLocationScopes(location, astScopes); + }, + async loadSourceText(sourceId) { + const source = getSource(getState(), sourceId).toJS(); + await dispatch(loadSourceText(source)); + } + } + ); + + dispatch({ + type: "MAP_SCOPES", + frame, + scopes: mappedScopes + }); + }; +} export function fetchScopes() { return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) { @@ -25,29 +53,27 @@ export function fetchScopes() { scopes }); - const sourceRecord = getSource( + const generatedSourceRecord = getSource( getState(), frame.generatedLocation.sourceId ); - if (sourceRecord.get("isWasm")) { + if (generatedSourceRecord.get("isWasm")) { return; } - if (isGeneratedId(frame.location.sourceId)) { + const sourceRecord = getSource(getState(), frame.location.sourceId); + + if (sourceRecord.get("isPrettyPrinted")) { return; } - const mappedScopes = await updateScopeBindings( - scopes, - frame.generatedLocation, - sourceMaps - ); + if (isGeneratedId(frame.location.sourceId)) { + return; + } - dispatch({ - type: "MAP_SCOPES", - frame, - scopes: mappedScopes - }); + if (features.mapScopes) { + dispatch(mapScopes(scopes, frame)); + } }; } diff --git a/src/actions/types.js b/src/actions/types.js index 45c98c5ec9..86188a2d21 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -261,8 +261,8 @@ export type ProjectTextSearchAction = { type: "ADD_SEARCH_RESULT", result: ProjectTextSearchResult } & { - type: "CLEAR_QUERY" - }; + type: "CLEAR_QUERY" +}; export type FileTextSearchAction = | { diff --git a/src/components/SecondaryPanes/Scopes.js b/src/components/SecondaryPanes/Scopes.js index 92adfcef2d..93ea321b60 100644 --- a/src/components/SecondaryPanes/Scopes.js +++ b/src/components/SecondaryPanes/Scopes.js @@ -13,11 +13,11 @@ import { getFrameScope, getPause } from "../../selectors"; -import { getScopes } from "../../utils/scopes"; +import { getScopes } from "../../utils/pause/scopes"; import { ObjectInspector } from "devtools-reps"; import type { Pause, LoadedObject } from "debugger-html"; -import type { NamedValue } from "../../utils/scopes"; +import type { NamedValue } from "../../utils/pause/scopes/types"; import "./Scopes.css"; diff --git a/src/test/mochitest/browser.ini b/src/test/mochitest/browser.ini index 0ae45a9d5d..aea26b26e8 100644 --- a/src/test/mochitest/browser.ini +++ b/src/test/mochitest/browser.ini @@ -11,6 +11,10 @@ support-files = examples/sourcemaps2/main.min.js examples/sourcemaps2/main.js examples/sourcemaps2/main.js.map + examples/sourcemaps3/bundle.js + examples/sourcemaps3/bundle.js.map + examples/sourcemaps3/sorted.js + examples/sourcemaps3/test.js examples/wasm-sourcemaps/average.js examples/wasm-sourcemaps/average.wasm examples/wasm-sourcemaps/average.wasm.map @@ -35,6 +39,7 @@ support-files = examples/doc-minified2.html examples/doc-sourcemaps.html examples/doc-sourcemaps2.html + examples/doc-sourcemaps3.html examples/doc-sourcemap-bogus.html examples/doc-sources.html examples/doc-return-values.html @@ -104,6 +109,7 @@ skip-if = true # regular failures during release in Bug 1415300 [browser_dbg-sourcemaps.js] [browser_dbg-sourcemaps-reloading.js] [browser_dbg-sourcemaps2.js] +[browser_dbg-sourcemaps3.js] [browser_dbg-sourcemaps-bogus.js] [browser_dbg-sources.js] [browser_dbg-tabs.js] diff --git a/src/test/mochitest/browser_dbg-sourcemaps3.js b/src/test/mochitest/browser_dbg-sourcemaps3.js new file mode 100644 index 0000000000..ed73e82e3a --- /dev/null +++ b/src/test/mochitest/browser_dbg-sourcemaps3.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests loading sourcemapped sources, setting breakpoints, and +// inspecting restored scopes. + +function toggleNode(dbg, index) { + clickElement(dbg, "scopeNode", index); +} + +function getLabel(dbg, index) { + return findElement(dbg, "scopeNode", index).innerText; +} + +function hasScopeNode(dbg, index) { + return !!findElement(dbg, "scopeNode", index); +} + +async function waitForScopeNode(dbg, index) { + const selector = getSelector("scopeNode", index); + return waitForElement(dbg, selector); +} + +// This source map does not have source contents, so it's fetched separately +add_task(async function() { + // NOTE: the CORS call makes the test run times inconsistent + requestLongerTimeout(2); + + const dbg = await initDebugger("doc-sourcemaps3.html"); + const { selectors: { getBreakpoint, getBreakpoints }, getState } = dbg; + + toggleScopes(dbg); + + await waitForSources(dbg, "bundle.js", "sorted.js", "test.js"); + + ok(true, "Original sources exist"); + const sortedSrc = findSource(dbg, "sorted.js"); + + await selectSource(dbg, sortedSrc); + + // Test that breakpoint is not off by a line. + await addBreakpoint(dbg, sortedSrc, 9); + is(getBreakpoints(getState()).size, 1, "One breakpoint exists"); + ok( + getBreakpoint(getState(), { sourceId: sortedSrc.id, line: 9, column: 4 }), + "Breakpoint has correct line" + ); + + invokeInTab("test"); + + await waitForPaused(dbg); + assertPausedLocation(dbg); + + await waitForDispatch(dbg, "MAP_SCOPES"); + + is(getLabel(dbg, 1), "Block"); + is(getLabel(dbg, 2), ""); + is(getLabel(dbg, 3), "na"); + is(getLabel(dbg, 4), "nb"); + + is(getLabel(dbg, 5), "Block"); + is( + hasScopeNode(dbg, 8) && !hasScopeNode(dbg, 9), + true, + "scope count before expand" + ); + toggleNode(dbg, 5); + + await waitForScopeNode(dbg, 9); + + is(getLabel(dbg, 6), "ma"); + is(getLabel(dbg, 7), "mb"); + + is( + hasScopeNode(dbg, 10) && !hasScopeNode(dbg, 11), + true, + "scope count before expand" + ); + toggleNode(dbg, 8); + + await waitForScopeNode(dbg, 11); + + is(getLabel(dbg, 9), "a"); + is(getLabel(dbg, 10), "arguments"); + is(getLabel(dbg, 11), "b"); +}); diff --git a/src/test/mochitest/examples/doc-sourcemaps3.html b/src/test/mochitest/examples/doc-sourcemaps3.html new file mode 100644 index 0000000000..c483608d17 --- /dev/null +++ b/src/test/mochitest/examples/doc-sourcemaps3.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/test/mochitest/examples/sourcemaps3/.babelrc b/src/test/mochitest/examples/sourcemaps3/.babelrc new file mode 100644 index 0000000000..01fd19076e --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/.babelrc @@ -0,0 +1 @@ + { "presets": [ "es2015" ] } diff --git a/src/test/mochitest/examples/sourcemaps3/.gitignore b/src/test/mochitest/examples/sourcemaps3/.gitignore new file mode 100644 index 0000000000..504afef81f --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/src/test/mochitest/examples/sourcemaps3/bundle.js b/src/test/mochitest/examples/sourcemaps3/bundle.js new file mode 100644 index 0000000000..a16c05500d --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/bundle.js @@ -0,0 +1,2 @@ +!function(n){function e(t){if(r[t])return r[t].exports;var o=r[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var r={};e.m=n,e.c=r,e.d=function(n,r,t){e.o(n,r)||Object.defineProperty(n,r,{configurable:!1,enumerable:!0,get:t})},e.n=function(n){var r=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(r,"a",r),r},e.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},e.p="",e(e.s=0)}([function(n,e,r){"use strict";var t=r(1);window.test=function(){var n=["b (30)","a","b (5)","z"],e=(0,t.fancySort)(n);console.log(e)}},function(n,e,r){"use strict";function t(n,e){var r=/.(\d+)\W*$/.exec(n),t=/.(\d+)\W*$/.exec(e);if(null==r||null==t||r[1]==t[1])return ne?1:0;var o=+r[1],u=+t[1];return ou?1:0}function o(n,e,r){if(0==n.length)return{found:!1,index:0};for(var t=0,o=n.length-1;t b ? 1 : 0;\n } else {\n var na = +ma[1],\n nb = +mb[1];\n return na < nb ? -1 : na > nb ? 1 : 0;\n }\n}\n\nfunction binaryLookup(ar, i, comparer) {\n if (ar.length == 0) {\n return { found: false, index: 0 };\n }\n var l = 0,\n r = ar.length - 1;\n while (l < r) {\n var mid = Math.floor((l + r) / 2);\n if (comparer(ar[mid], i) < 0) {\n l = mid + 1;\n } else {\n r = mid;\n }\n }\n var result = comparer(ar[l], i);\n if (result === 0) {\n return { found: true, index: l };\n }\n return {\n found: false,\n index: result < 0 ? l + 1 : l\n };\n}\n\nfunction fancySort(input) {\n return input.reduce(function (ar, i) {\n var _binaryLookup = binaryLookup(ar, i, comparer),\n index = _binaryLookup.index;\n\n ar.splice(index, 0, i);\n return ar;\n }, []);\n}\n\n/***/ })\n/******/ ]);\n\n\n// WEBPACK FOOTER //\n// bundle.js"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 87d8b22c3e8e915d4367","import { fancySort } from \"./sorted.js\";\n\nwindow.test = function() {\n let test = [\"b (30)\", \"a\", \"b (5)\", \"z\"];\n let result = fancySort(test);\n console.log(result);\n};\n\n\n\n// WEBPACK FOOTER //\n// ./test.js","function comparer(a, b) {\n const ma = /.(\\d+)\\W*$/.exec(a);\n const mb = /.(\\d+)\\W*$/.exec(b);\n if (ma == null || mb == null || ma[1] == mb[1]) {\n return a < b ? -1 : a > b ? 1 : 0;\n } else {\n const na = +ma[1],\n nb = +mb[1];\n return na < nb ? -1 : na > nb ? 1 : 0;\n }\n}\n\nfunction binaryLookup(ar, i, comparer) {\n if (ar.length == 0) {\n return { found: false, index: 0 };\n }\n let l = 0,\n r = ar.length - 1;\n while (l < r) {\n const mid = Math.floor((l + r) / 2);\n if (comparer(ar[mid], i) < 0) {\n l = mid + 1;\n } else {\n r = mid;\n }\n }\n const result = comparer(ar[l], i);\n if (result === 0) {\n return { found: true, index: l };\n }\n return {\n found: false,\n index: result < 0 ? l + 1 : l\n };\n}\n\nexport function fancySort(input) {\n return input.reduce((ar, i) => {\n const { index } = binaryLookup(ar, i, comparer);\n ar.splice(index, 0, i);\n return ar;\n }, []);\n}\n\n\n\n// WEBPACK FOOTER //\n// ./sorted.js"],"sourceRoot":""} \ No newline at end of file diff --git a/src/test/mochitest/examples/sourcemaps3/package.json b/src/test/mochitest/examples/sourcemaps3/package.json new file mode 100644 index 0000000000..08352aa083 --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/package.json @@ -0,0 +1,19 @@ +{ + "name": "sorted-es6", + "version": "1.0.0", + "description": "", + "main": "sorted.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": {}, + "devDependencies": { + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-preset-es2015": "^6.24.1", + "webpack": "^3.7.1" + } +} diff --git a/src/test/mochitest/examples/sourcemaps3/sorted.js b/src/test/mochitest/examples/sourcemaps3/sorted.js new file mode 100644 index 0000000000..2621b8f5a4 --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/sorted.js @@ -0,0 +1,43 @@ +function comparer(a, b) { + const ma = /.(\d+)\W*$/.exec(a); + const mb = /.(\d+)\W*$/.exec(b); + if (ma == null || mb == null || ma[1] == mb[1]) { + return a < b ? -1 : a > b ? 1 : 0; + } else { + const na = +ma[1], + nb = +mb[1]; + return na < nb ? -1 : na > nb ? 1 : 0; + } +} + +function binaryLookup(ar, i, comparer) { + if (ar.length == 0) { + return { found: false, index: 0 }; + } + let l = 0, + r = ar.length - 1; + while (l < r) { + const mid = Math.floor((l + r) / 2); + if (comparer(ar[mid], i) < 0) { + l = mid + 1; + } else { + r = mid; + } + } + const result = comparer(ar[l], i); + if (result === 0) { + return { found: true, index: l }; + } + return { + found: false, + index: result < 0 ? l + 1 : l + }; +} + +export function fancySort(input) { + return input.reduce((ar, i) => { + const { index } = binaryLookup(ar, i, comparer); + ar.splice(index, 0, i); + return ar; + }, []); +} diff --git a/src/test/mochitest/examples/sourcemaps3/test.js b/src/test/mochitest/examples/sourcemaps3/test.js new file mode 100644 index 0000000000..2c30036d68 --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/test.js @@ -0,0 +1,7 @@ +import { fancySort } from "./sorted.js"; + +window.test = function() { + let test = ["b (30)", "a", "b (5)", "z"]; + let result = fancySort(test); + console.log(result); +}; diff --git a/src/test/mochitest/examples/sourcemaps3/webpack.config.js b/src/test/mochitest/examples/sourcemaps3/webpack.config.js new file mode 100644 index 0000000000..c4b5f486d1 --- /dev/null +++ b/src/test/mochitest/examples/sourcemaps3/webpack.config.js @@ -0,0 +1,31 @@ +const path = require("path"); +const webpack = require("webpack"); + +module.exports = { + entry: "./test.js", + output: { + path: __dirname, + filename: "bundle.js" + }, + devtool: "sourcemap", + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader" + } + ] + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false + }, + sourceMap: true, + output: { + comments: false + } + }) + ] +}; diff --git a/src/utils/pause.js b/src/utils/pause.js index 96b096b42a..8cccee8150 100644 --- a/src/utils/pause.js +++ b/src/utils/pause.js @@ -6,8 +6,16 @@ import type { Pause, Frame, Location } from "../types"; import { get } from "lodash"; import { getScopes } from "../workers/parser"; +// eslint-disable-next-line max-len +import { updateScopeBindings as coreUpdateScopeBindings } from "devtools-map-bindings/src/updateScopeBindings"; -import type { Scope, MappedScopeBindings, Why } from "debugger-html"; +import type { + MappedScopeBindings, + Scope, + SourceId, + SourceScope, + Why +} from "debugger-html"; export function updateFrameLocations( frames: Frame[], @@ -28,38 +36,30 @@ export function updateFrameLocations( ); } -function extendScope( - scope: ?Scope, - generatedScopes: MappedScopeBindings[], - index: number -): ?Scope { - if (!scope) { - return undefined; - } - if (index >= generatedScopes.length) { - return scope; - } - return { - ...scope, - parent: extendScope(scope.parent, generatedScopes, index + 1), - sourceBindings: generatedScopes[index].bindings - }; -} +type UpdateScopeBindingsContextArg = { + getLocationScopes: ( + location: Location, + astScopes: ?(SourceScope[]) + ) => Promise, + loadSourceText: (sourceId: SourceId) => Promise +}; export async function updateScopeBindings( - scope: any, - location: Location, - sourceMaps: any -) { - const astScopes = await getScopes(location); - const generatedScopes = await sourceMaps.getLocationScopes( - location, - astScopes - ); - if (!generatedScopes) { - return scope; - } - return extendScope(scope, generatedScopes, 0); + scope: ?Scope, + generatedLocation: Location, + originalLocation: Location, + { getLocationScopes, loadSourceText }: UpdateScopeBindingsContextArg +): Promise { + return coreUpdateScopeBindings(scope, generatedLocation, originalLocation, { + async getSourceMapsScopes(location) { + const astScopes: ?(SourceScope[]) = await getScopes(location); + return getLocationScopes(location, astScopes); + }, + async getOriginalSourceScopes(location) { + await loadSourceText(location.sourceId); + return getScopes(location); + } + }); } // Map protocol pause "why" reason to a valid L10N key diff --git a/src/utils/pause/scopes/getScope.js b/src/utils/pause/scopes/getScope.js new file mode 100644 index 0000000000..5fa920d60e --- /dev/null +++ b/src/utils/pause/scopes/getScope.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// @flow +import { getBindingVariables, getSourceBindingVariables } from "./getVariables"; +import { getFramePopVariables, getThisVariable } from "./utils"; +import { simplifyDisplayName } from "../../frame"; + +import type { Frame, Pause, Scope } from "debugger-html"; + +import type { NamedValue } from "./types"; + +function getScopeTitle(type, scope) { + if (type === "function") { + return scope.function.displayName + ? simplifyDisplayName(scope.function.displayName) + : L10N.getStr("anonymous"); + } + return L10N.getStr("scopes.block"); +} + +export function getScope( + scope: Scope, + selectedFrame: Frame, + frameScopes: Scope, + pauseInfo: Pause, + scopeIndex: number +): ?NamedValue { + const { type, actor } = scope; + + const isLocalScope = scope.actor === frameScopes.actor; + + const key = `${actor}-${scopeIndex}`; + if (type === "function" || type === "block") { + const bindings = scope.bindings; + const sourceBindings = scope.sourceBindings; + + let vars = sourceBindings + ? getSourceBindingVariables(bindings, sourceBindings, key) + : getBindingVariables(bindings, key); + + // show exception, return, and this variables in innermost scope + if (isLocalScope) { + vars = vars.concat(getFramePopVariables(pauseInfo, key)); + + const this_ = getThisVariable(selectedFrame, key); + + if (this_) { + vars.push(this_); + } + } + + if (vars && vars.length) { + const title = getScopeTitle(type, scope); + vars.sort((a, b) => a.name.localeCompare(b.name)); + return { + name: title, + path: key, + contents: vars + }; + } + } else if (type === "object") { + let value = scope.object; + // If this is the global window scope, mark it as such so that it will + // preview Window: Global instead of Window: Window + if (value.class === "Window") { + value = { ...scope.object, displayClass: "Global" }; + } + return { + name: scope.object.class, + path: key, + contents: { value } + }; + } + + return null; +} diff --git a/src/utils/pause/scopes/getVariables.js b/src/utils/pause/scopes/getVariables.js new file mode 100644 index 0000000000..3d4a78ad77 --- /dev/null +++ b/src/utils/pause/scopes/getVariables.js @@ -0,0 +1,78 @@ +/* eslint max-nested-callbacks: ["error", 4] */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// @flow +import { toPairs } from "lodash"; + +import type { NamedValue } from "./types"; +import type { BindingContents, ScopeBindings } from "debugger-html"; + +// VarAndBindingsPair actually is [name: string, contents: BindingContents] +type VarAndBindingsPair = Array; +type VarAndBindingsPairs = Array; + +// Scope's bindings field which holds variables and arguments +type ScopeBindingsWrapper = { + variables: ScopeBindings, + arguments: BindingContents[] +}; + +export function getSourceBindingVariables( + bindings: ScopeBindingsWrapper, + sourceBindings: { + [originalName: string]: string + }, + parentName: string +): NamedValue[] { + const result = getBindingVariables(bindings, parentName); + const index: any = Object.create(null); + result.forEach(entry => { + index[entry.name] = { used: false, entry }; + }); + // Find and replace variables that is present in sourceBindings. + const bound = Object.keys(sourceBindings).map(name => { + const generatedName = sourceBindings[name]; + const foundMap = index[generatedName]; + let contents; + if (foundMap) { + foundMap.used = true; + contents = foundMap.entry.contents; + } else { + contents = { value: { type: "undefined" } }; + } + return { + name, + generatedName, + path: `${parentName}/${generatedName}`, + contents + }; + }); + // Use rest of them (not found in the sourceBindings) as is. + const unused = result.filter(entry => !index[entry.name].used); + return bound.concat(unused); +} + +// Create the tree nodes representing all the variables and arguments +// for the bindings from a scope. +export function getBindingVariables( + bindings: ScopeBindingsWrapper, + parentName: string +): NamedValue[] { + const args: VarAndBindingsPairs = bindings.arguments.map( + arg => toPairs(arg)[0] + ); + const variables: VarAndBindingsPairs = toPairs(bindings.variables); + + return args.concat(variables).map(binding => { + const name = (binding[0]: string); + const contents = (binding[1]: BindingContents); + return { + name, + path: `${parentName}/${name}`, + contents + }; + }); +} diff --git a/src/utils/pause/scopes/index.js b/src/utils/pause/scopes/index.js new file mode 100644 index 0000000000..4249173808 --- /dev/null +++ b/src/utils/pause/scopes/index.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// @flow + +import { synthesizeScopes } from "./synthesizeScopes"; +import { getScope } from "./getScope"; + +import type { Frame, Pause, Scope, BindingContents } from "debugger-html"; + +export type NamedValue = { + name: string, + generatedName?: string, + path: string, + contents: BindingContents | NamedValue[] +}; + +export function getScopes( + pauseInfo: Pause, + selectedFrame: Frame, + frameScopes: ?Scope +): ?(NamedValue[]) { + if (!pauseInfo || !selectedFrame) { + return null; + } + + if (!frameScopes) { + return null; + } + + const scopes = []; + + let scope = frameScopes; + let scopeIndex = 1; + + while (scope) { + const { syntheticScopes } = scope; + let lastScope = scope; + + if (!syntheticScopes) { + const scopeItem = getScope( + scope, + selectedFrame, + frameScopes, + pauseInfo, + scopeIndex + ); + + if (scopeItem) { + scopes.push(scopeItem); + } + scopeIndex++; + } else { + scopes.push( + ...synthesizeScopes( + scope, + selectedFrame, + frameScopes, + pauseInfo, + scopeIndex + ) + ); + + // skip to the next generated scope + const scopeDepth = syntheticScopes.groupLength; + for (let i = 1; lastScope.parent && i < scopeDepth; i++) { + const nextScope = lastScope.parent; + lastScope = nextScope; + } + + scope = lastScope; + scopeIndex += syntheticScopes.scopes.length; + } + scope = scope.parent; + } + + return scopes; +} diff --git a/src/utils/pause/scopes/synthesizeScopes.js b/src/utils/pause/scopes/synthesizeScopes.js new file mode 100644 index 0000000000..bd7b6cf5b7 --- /dev/null +++ b/src/utils/pause/scopes/synthesizeScopes.js @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// @flow + +import { getBindingVariables } from "./getVariables"; +import { simplifyDisplayName } from "../../frame"; +import { getFramePopVariables, getThisVariable } from "./utils"; + +import type { Frame, Pause, Scope, SyntheticScope } from "debugger-html"; + +import type { NamedValue } from "./types"; + +function getSynteticScopeTitle(type, generatedScopes) { + if (type === "function") { + // FIXME Use original function name here + const lastGeneratedScope = generatedScopes[generatedScopes.length - 1]; + const isLastGeneratedScopeFn = + lastGeneratedScope && lastGeneratedScope.type === "function"; + return isLastGeneratedScopeFn && lastGeneratedScope.function.displayName + ? simplifyDisplayName(lastGeneratedScope.function.displayName) + : L10N.getStr("anonymous"); + } + return L10N.getStr("scopes.block"); +} + +function findOriginalBindings( + bindingsNames, + generatedScopes, + key, + foundGeneratedNames +) { + return bindingsNames.reduce((vars, name) => { + // Find binding name in the original source bindings + const generatedScope = generatedScopes.find( + gs => gs.sourceBindings && name in gs.sourceBindings + ); + if (!generatedScope || !generatedScope.sourceBindings) { + return vars; + } + // .. and map it to the generated name + const generatedName = generatedScope.sourceBindings[name]; + // Skip if we already use the generated name + if (generatedName && !foundGeneratedNames[generatedName]) { + if (generatedScope.bindings.variables[generatedName]) { + vars.push({ + name, + generatedName, + path: `${key}/${generatedName}`, + contents: generatedScope.bindings.variables[generatedName] + }); + foundGeneratedNames[generatedName] = true; + return vars; + } + + const arg = generatedScope.bindings.arguments.find( + arg_ => arg_[generatedName] + ); + if (arg) { + vars.push({ + name, + generatedName, + path: `${key}/${generatedName}`, + contents: arg[generatedName] + }); + foundGeneratedNames[generatedName] = true; + return vars; + } + } + + vars.push({ + name, + generatedName, + path: `${key}/${generatedName}`, + contents: { value: { type: "undefined" } } + }); + return vars; + }, []); +} + +function findUnusedBindings(generatedScopes, foundGeneratedNames, key) { + const allGeneratedVars = generatedScopes.reduce((acc, { bindings }) => { + return acc.concat(getBindingVariables(bindings, key)); + }, []); + return allGeneratedVars.filter(v => !foundGeneratedNames[v.name]); +} + +// Create a synthesized scope based on its binding names and +// generated/original scopes information. +function synthesizeScope( + syntheticScope: SyntheticScope, + index: number, + actor: string, + key: string, + scopeIndex: number, + lastScopeIndex: number, + generatedScopes: Scope[], + foundGeneratedNames: { [name: string]: boolean }, + scope, + frameScopes: Scope, + selectedFrame: Frame, + pauseInfo: Pause +): NamedValue[] { + const { bindingsNames } = syntheticScope; + const isLast = index === lastScopeIndex; + + let vars = findOriginalBindings( + bindingsNames, + generatedScopes, + key, + foundGeneratedNames + ); + + if (isLast) { + // For the last synthesized scope, apply all generated names we did not use + vars = [ + ...vars, + ...findUnusedBindings(generatedScopes, foundGeneratedNames, key) + ]; + } + + if (index === 0) { + const isLocalScope = scope.actor === frameScopes.actor; + + // For the first synthesized scope, add this and other vars. + if (isLocalScope) { + vars = [...vars, ...getFramePopVariables(pauseInfo, key)]; + + const this_ = getThisVariable(selectedFrame, key); + + if (this_) { + vars.push(this_); + } + } + } + + return vars; +} + +export function synthesizeScopes( + scope: Scope, + selectedFrame: Frame, + frameScopes: Scope, + pauseInfo: Pause, + scopeIndex: number +): NamedValue[] { + const { actor, syntheticScopes } = scope; + if (!syntheticScopes) { + return []; + } + + // Collect all connected generated scopes. + const generatedScopes = []; + for ( + let count = syntheticScopes.groupLength, s = scope; + count > 0 && s; + count-- + ) { + generatedScopes.push(s); + s = s.parent; + } + + const foundGeneratedNames = (Object.create(null): any); + const lastScopeIndex = syntheticScopes.scopes.length - 1; + return syntheticScopes.scopes.reduce((result, syntheticScope, index) => { + const key = `${actor}-${scopeIndex + index}`; + const bindings = synthesizeScope( + syntheticScope, + index, + actor, + key, + scopeIndex, + lastScopeIndex, + generatedScopes, + foundGeneratedNames, + scope, + frameScopes, + selectedFrame, + pauseInfo + ); + + if (bindings && bindings.length) { + const title = getSynteticScopeTitle(syntheticScope.type, generatedScopes); + bindings.sort((a, b) => a.name.localeCompare(b.name)); + result.push({ + name: title, + path: key, + contents: bindings + }); + } + return result; + }, []); +} diff --git a/src/utils/pause/scopes/tests/getFramePopVariables.spec.js b/src/utils/pause/scopes/tests/getFramePopVariables.spec.js new file mode 100644 index 0000000000..92236b50e8 --- /dev/null +++ b/src/utils/pause/scopes/tests/getFramePopVariables.spec.js @@ -0,0 +1,105 @@ +/* eslint max-nested-callbacks: ["error", 4] */ + +import { getFramePopVariables } from "../utils"; + +const errorGrip = { + type: "object", + actor: "server2.conn66.child1/pausedobj243", + class: "Error", + extensible: true, + frozen: false, + sealed: false, + ownPropertyLength: 4, + preview: { + kind: "Error", + name: "Error", + message: "blah", + stack: + "onclick@http://localhost:8000/examples/doc-return-values.html:1:18\n", + fileName: "http://localhost:8000/examples/doc-return-values.html", + lineNumber: 1, + columnNumber: 18 + } +}; + +function returnWhy(grip) { + return { + why: { + type: "resumeLimit", + frameFinished: { + return: grip + } + } + }; +} + +function throwWhy(grip) { + return { + why: { + type: "resumeLimit", + frameFinished: { + throw: grip + } + } + }; +} +describe("pause - scopes", () => { + describe("getFramePopVariables", () => { + describe("falsey values", () => { + // NOTE: null and undefined are treated like objects and given a type + const falsey = { false: false, "0": 0, null: { type: "null" } }; + for (const test in falsey) { + const value = falsey[test]; + it(`shows ${test} returns`, () => { + const pauseData = returnWhy(value); + const vars = getFramePopVariables(pauseData, ""); + expect(vars[0].name).toEqual(""); + expect(vars[0].name).toEqual(""); + expect(vars[0].contents.value).toEqual(value); + }); + + it(`shows ${test} throws`, () => { + const pauseData = throwWhy(value); + const vars = getFramePopVariables(pauseData, ""); + expect(vars[0].name).toEqual(""); + expect(vars[0].name).toEqual(""); + expect(vars[0].contents.value).toEqual(value); + }); + } + }); + + describe("Error / Objects", () => { + it("shows Error returns", () => { + const pauseData = returnWhy(errorGrip); + const vars = getFramePopVariables(pauseData, ""); + expect(vars[0].name).toEqual(""); + expect(vars[0].name).toEqual(""); + expect(vars[0].contents.value.class).toEqual("Error"); + }); + + it("shows error throws", () => { + const pauseData = throwWhy(errorGrip); + const vars = getFramePopVariables(pauseData, ""); + expect(vars[0].name).toEqual(""); + expect(vars[0].name).toEqual(""); + expect(vars[0].contents.value.class).toEqual("Error"); + }); + }); + + describe("undefined", () => { + it("does not show undefined returns", () => { + const pauseData = returnWhy({ type: "undefined" }); + const vars = getFramePopVariables(pauseData, ""); + expect(vars.length).toEqual(0); + }); + + it("shows undefined throws", () => { + const pauseData = throwWhy({ type: "undefined" }); + const vars = getFramePopVariables(pauseData, ""); + expect(vars[0].name).toEqual(""); + expect(vars[0].name).toEqual(""); + expect(vars[0].contents.value).toEqual({ type: "undefined" }); + }); + }); + }); +}); diff --git a/src/utils/tests/scopes.spec.js b/src/utils/pause/scopes/tests/scopes.spec.js similarity index 50% rename from src/utils/tests/scopes.spec.js rename to src/utils/pause/scopes/tests/scopes.spec.js index dc451f3651..8b07c4be22 100644 --- a/src/utils/tests/scopes.spec.js +++ b/src/utils/pause/scopes/tests/scopes.spec.js @@ -1,107 +1,6 @@ -import { getFramePopVariables, getScopes } from "../scopes"; - -const errorGrip = { - type: "object", - actor: "server2.conn66.child1/pausedobj243", - class: "Error", - extensible: true, - frozen: false, - sealed: false, - ownPropertyLength: 4, - preview: { - kind: "Error", - name: "Error", - message: "blah", - stack: - "onclick@http://localhost:8000/examples/doc-return-values.html:1:18\n", - fileName: "http://localhost:8000/examples/doc-return-values.html", - lineNumber: 1, - columnNumber: 18 - } -}; - -function returnWhy(grip) { - return { - why: { - type: "resumeLimit", - frameFinished: { - return: grip - } - } - }; -} - -function throwWhy(grip) { - return { - why: { - type: "resumeLimit", - frameFinished: { - throw: grip - } - } - }; -} +import { getScopes } from ".."; describe("scopes", () => { - describe("getFramePopVariables", () => { - describe("falsey values", () => { - // NOTE: null and undefined are treated like objects and given a type - const falsey = { false: false, "0": 0, null: { type: "null" } }; - for (const test in falsey) { - const value = falsey[test]; - it(`shows ${test} returns`, () => { - const pauseData = returnWhy(value); - const vars = getFramePopVariables(pauseData, ""); - expect(vars[0].name).toEqual(""); - expect(vars[0].name).toEqual(""); - expect(vars[0].contents.value).toEqual(value); - }); - - it(`shows ${test} throws`, () => { - const pauseData = throwWhy(value); - const vars = getFramePopVariables(pauseData, ""); - expect(vars[0].name).toEqual(""); - expect(vars[0].name).toEqual(""); - expect(vars[0].contents.value).toEqual(value); - }); - } - }); - - describe("Error / Objects", () => { - it("shows Error returns", () => { - const pauseData = returnWhy(errorGrip); - const vars = getFramePopVariables(pauseData, ""); - expect(vars[0].name).toEqual(""); - expect(vars[0].name).toEqual(""); - expect(vars[0].contents.value.class).toEqual("Error"); - }); - - it("shows error throws", () => { - const pauseData = throwWhy(errorGrip); - const vars = getFramePopVariables(pauseData, ""); - expect(vars[0].name).toEqual(""); - expect(vars[0].name).toEqual(""); - expect(vars[0].contents.value.class).toEqual("Error"); - }); - }); - - describe("undefined", () => { - it("does not show undefined returns", () => { - const pauseData = returnWhy({ type: "undefined" }); - const vars = getFramePopVariables(pauseData, ""); - expect(vars.length).toEqual(0); - }); - - it("shows undefined throws", () => { - const pauseData = throwWhy({ type: "undefined" }); - const vars = getFramePopVariables(pauseData, ""); - expect(vars[0].name).toEqual(""); - expect(vars[0].name).toEqual(""); - expect(vars[0].contents.value).toEqual({ type: "undefined" }); - }); - }); - }); - describe("getScopes", () => { it("single scope", () => { const pauseData = { @@ -123,7 +22,8 @@ describe("scopes", () => { this: {} }; - const scopes = getScopes(pauseData, selectedFrame); + const frameScopes = selectedFrame.scope; + const scopes = getScopes(pauseData, selectedFrame, frameScopes); expect(scopes[0].path).toEqual("actor1-1"); expect(scopes[0].contents[0]).toEqual({ name: "", @@ -161,7 +61,8 @@ describe("scopes", () => { this: {} }; - const scopes = getScopes(pauseData, selectedFrame); + const frameScopes = selectedFrame.scope; + const scopes = getScopes(pauseData, selectedFrame, frameScopes); expect(scopes[1].path).toEqual("actor2-2"); expect(scopes[1].contents[0]).toEqual({ name: "foo", @@ -195,7 +96,8 @@ describe("scopes", () => { this: {} }; - const scopes = getScopes(pauseData, selectedFrame); + const frameScopes = selectedFrame.scope; + const scopes = getScopes(pauseData, selectedFrame, frameScopes); expect(scopes).toMatchObject([ { path: "actor1-1", @@ -244,7 +146,8 @@ describe("scopes", () => { this: {} }; - const scopes = getScopes(pauseData, selectedFrame); + const frameScopes = selectedFrame.scope; + const scopes = getScopes(pauseData, selectedFrame, frameScopes); expect(scopes).toMatchObject([ { path: "actor1-1", @@ -267,5 +170,94 @@ describe("scopes", () => { } ]); }); + + it("synthetic scope", () => { + const pauseData = { + frame: { + scope: { + actor: "actor1" + }, + this: {} + } + }; + + const syntheticScopes = [ + { + type: "block", + bindingsNames: ["first"] + }, + { + type: "block", + bindingsNames: ["second"] + }, + { + type: "block", + bindingsNames: [] + } + ]; + + const selectedFrame = { + scope: { + actor: "actor1", + type: "block", + sourceBindings: { + first: "a", + second: "b" + }, + bindings: { + arguments: [ + { + b: {} + } + ], + variables: { + a: {} + } + }, + syntheticScopes: { + groupIndex: 0, + groupLength: 2, + scopes: syntheticScopes + }, + parent: { + actor: "actor2", + type: "block", + bindings: { + arguments: [], + variables: { + foo: {} + } + }, + syntheticScopes: { + groupIndex: 1, + groupLength: 2, + scopes: syntheticScopes + } + } + }, + this: {} + }; + + const frameScopes = selectedFrame.scope; + const scopes = getScopes(pauseData, selectedFrame, frameScopes); + expect(scopes.length).toEqual(3); + expect(scopes[0].contents[1]).toEqual({ + name: "first", + generatedName: "a", + path: "actor1-1/a", + contents: {} + }); + expect(scopes[1].contents[0]).toEqual({ + name: "second", + generatedName: "b", + path: "actor1-2/b", + contents: {} + }); + expect(scopes[2].contents[0]).toEqual({ + name: "foo", + path: "actor1-3/foo", + contents: {} + }); + }); }); }); diff --git a/src/utils/pause/scopes/types.js b/src/utils/pause/scopes/types.js new file mode 100644 index 0000000000..c946f0bf91 --- /dev/null +++ b/src/utils/pause/scopes/types.js @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// @flow + +import type { BindingContents } from "debugger-html"; + +export type NamedValue = { + name: string, + generatedName?: string, + path: string, + contents: BindingContents | NamedValue[] +}; diff --git a/src/utils/pause/scopes/utils.js b/src/utils/pause/scopes/utils.js new file mode 100644 index 0000000000..e4f5f47164 --- /dev/null +++ b/src/utils/pause/scopes/utils.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// @flow + +import type { Pause } from "debugger-html"; +import type { NamedValue } from "./types"; + +export function getFramePopVariables( + pauseInfo: Pause, + path: string +): NamedValue[] { + const vars: Array = []; + + if (pauseInfo.why && pauseInfo.why.frameFinished) { + const frameFinished = pauseInfo.why.frameFinished; + + // Always display a `throw` property if present, even if it is falsy. + if (Object.prototype.hasOwnProperty.call(frameFinished, "throw")) { + vars.push({ + name: "", + path: `${path}/`, + contents: { value: frameFinished.throw } + }); + } + + if (Object.prototype.hasOwnProperty.call(frameFinished, "return")) { + const returned = frameFinished.return; + + // Do not display undefined. Do display falsy values like 0 and false. The + // protocol grip for undefined is a JSON object: { type: "undefined" }. + if (typeof returned !== "object" || returned.type !== "undefined") { + vars.push({ + name: "", + path: `${path}/`, + contents: { value: returned } + }); + } + } + } + + return vars; +} + +export function getThisVariable(frame: any, path: string): ?NamedValue { + const this_ = frame.this; + + if (!this_) { + return null; + } + + return { + name: "", + path: `${path}/`, + contents: { value: this_ } + }; +} diff --git a/src/utils/prefs.js b/src/utils/prefs.js index 868bc90873..9f9e206cd5 100644 --- a/src/utils/prefs.js +++ b/src/utils/prefs.js @@ -34,6 +34,7 @@ if (isDevelopment()) { pref("devtools.debugger.features.shortcuts", true); pref("devtools.debugger.features.root", true); pref("devtools.debugger.features.column-breakpoints", true); + pref("devtools.debugger.features.map-scopes", true); } export const prefs = new PrefsHelper("devtools", { @@ -61,7 +62,8 @@ export const features = new PrefsHelper("devtools.debugger.features", { wasm: ["Bool", "wasm", true], shortcuts: ["Bool", "shortcuts", true], root: ["Bool", "root", false], - columnBreakpoints: ["Bool", "column-breakpoints", false] + columnBreakpoints: ["Bool", "column-breakpoints", false], + mapScopes: ["Bool", "map-scopes", true] }); if (prefs.debuggerPrefsSchemaVersion !== prefsSchemaVersion) { diff --git a/src/utils/scopes.js b/src/utils/scopes.js deleted file mode 100644 index d6cc97e75e..0000000000 --- a/src/utils/scopes.js +++ /dev/null @@ -1,206 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at . */ - -// @flow - -import { toPairs } from "lodash"; -import { simplifyDisplayName } from "./frame"; -import type { Frame, Pause, Scope, BindingContents } from "debugger-html"; - -export type NamedValue = { - name: string, - generatedName?: string, - path: string, - contents: BindingContents | NamedValue[] -}; - -// VarAndBindingsPair actually is [name: string, contents: ScopeBindings] -type VarAndBindingsPair = Array; -type VarAndBindingsPairs = Array; - -// Create the tree nodes representing all the variables and arguments -// for the bindings from a scope. -function getBindingVariables(bindings, parentName) { - const args: VarAndBindingsPairs = bindings.arguments.map( - arg => toPairs(arg)[0] - ); - const variables: VarAndBindingsPairs = toPairs(bindings.variables); - - return args.concat(variables).map(binding => { - const name = (binding[0]: string); - const contents = (binding[1]: BindingContents); - return { - name, - path: `${parentName}/${name}`, - contents - }; - }); -} - -function getSourceBindingVariables( - bindings, - sourceBindings: { - [originalName: string]: string - }, - parentName: string -) { - const result = getBindingVariables(bindings, parentName); - const index: any = Object.create(null); - result.forEach(entry => { - index[entry.name] = { used: false, entry }; - }); - // Find and replace variables that is present in sourceBindings. - const bound = Object.keys(sourceBindings).map(name => { - const generatedName = sourceBindings[name]; - const foundMap = index[generatedName]; - let contents; - if (foundMap) { - foundMap.used = true; - contents = foundMap.entry.contents; - } else { - contents = { value: { type: "undefined" } }; - } - return { - name, - generatedName, - path: `${parentName}/${generatedName}`, - contents - }; - }); - // Use rest of them (not found in the sourceBindings) as is. - const unused = result.filter(entry => !index[entry.name].used); - return bound.concat(unused); -} - -export function getFramePopVariables(pauseInfo: Pause, path: string) { - const vars = []; - - if (pauseInfo.why && pauseInfo.why.frameFinished) { - const frameFinished = pauseInfo.why.frameFinished; - - // Always display a `throw` property if present, even if it is falsy. - if (Object.prototype.hasOwnProperty.call(frameFinished, "throw")) { - vars.push({ - name: "", - path: `${path}/`, - contents: { value: frameFinished.throw } - }); - } - - if (Object.prototype.hasOwnProperty.call(frameFinished, "return")) { - const returned = frameFinished.return; - - // Do not display undefined. Do display falsy values like 0 and false. The - // protocol grip for undefined is a JSON object: { type: "undefined" }. - if (typeof returned !== "object" || returned.type !== "undefined") { - vars.push({ - name: "", - path: `${path}/`, - contents: { value: returned } - }); - } - } - } - - return vars; -} - -function getThisVariable(frame: any, path: string) { - const this_ = frame.this; - - if (!this_) { - return null; - } - - return { - name: "", - path: `${path}/`, - contents: { value: this_ } - }; -} - -export function getScopes( - pauseInfo: Pause, - selectedFrame: Frame, - selectedScope: ?Scope -): ?(NamedValue[]) { - if (!pauseInfo || !selectedFrame) { - return null; - } - - // NOTE: it's possible that we're inspecting an old server - // that does not support getting frame scopes directly - selectedScope = selectedScope || selectedFrame.scope; - - if (!selectedScope) { - return null; - } - - const scopes = []; - - let scope = selectedScope; - let scopeIndex = 1; - - do { - const { type, actor } = scope; - const key = `${actor}-${scopeIndex}`; - if (type === "function" || type === "block") { - const bindings = scope.bindings; - const sourceBindings = scope.sourceBindings; - let title; - if (type === "function") { - title = scope.function.displayName - ? simplifyDisplayName(scope.function.displayName) - : L10N.getStr("anonymous"); - } else { - title = L10N.getStr("scopes.block"); - } - - let vars = sourceBindings - ? getSourceBindingVariables(bindings, sourceBindings, key) - : getBindingVariables(bindings, key); - - // On the innermost scope of a frame that is just about to be popped, show - // the return value or the exception being thrown as special variables. - if ( - scope.actor === selectedScope.actor && - selectedFrame.id === pauseInfo.frame.id - ) { - vars = vars.concat(getFramePopVariables(pauseInfo, key)); - } - - if (scope.actor === selectedScope.actor) { - const this_ = getThisVariable(selectedFrame, key); - - if (this_) { - vars.push(this_); - } - } - - if (vars && vars.length) { - vars.sort((a, b) => a.name.localeCompare(b.name)); - scopes.push({ - name: title, - path: key, - contents: vars - }); - } - } else if (type === "object") { - let value = scope.object; - // If this is the global window scope, mark it as such so that it will - // preview Window: Global instead of Window: Window - if (value.class === "Window") { - value = { ...scope.object, displayClass: "Global" }; - } - scopes.push({ - name: scope.object.class, - path: key, - contents: { value } - }); - } - scopeIndex++; - } while ((scope = scope.parent)); // eslint-disable-line no-cond-assign - - return scopes; -} diff --git a/yarn.lock b/yarn.lock index c1933c28a0..91f8cb26db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2988,9 +2988,9 @@ devtools-linters@^0.0.3: retext-syntax-urls "^1.0.0" unified "^6.1.0" -devtools-map-bindings@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/devtools-map-bindings/-/devtools-map-bindings-0.3.0.tgz#239eaf807b9a8680ceaf1491af1f7f2d4af4500e" +devtools-map-bindings@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/devtools-map-bindings/-/devtools-map-bindings-0.3.2.tgz#cfd37d37f544844d77e55cb877e057a17dbbf739" dependencies: babel-plugin-transform-async-to-generator "^6.22.0" babel-plugin-transform-flow-strip-types "^6.22.0" @@ -3047,13 +3047,13 @@ devtools-source-editor@^0.0.8: dependencies: codemirror "^5.28.0" -devtools-source-map@^0.14.5: - version "0.14.5" - resolved "https://registry.yarnpkg.com/devtools-source-map/-/devtools-source-map-0.14.5.tgz#e95f1fa3bae47e56002b8d3750f96afafff8e072" +devtools-source-map@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/devtools-source-map/-/devtools-source-map-0.14.7.tgz#b0ad5993dca69c27991a7f06fbc3b3a0c2250044" dependencies: babel-plugin-transform-async-to-generator "^6.22.0" babel-plugin-transform-flow-strip-types "^6.22.0" - devtools-map-bindings "^0.3.0" + devtools-map-bindings "^0.3.2" devtools-utils "0.0.9" jest "^19.0.2" md5 "^2.2.1"