fune/devtools/client/debugger/new/src/utils/pause/mapScopes/index.js

395 lines
No EOL
14 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.buildMappedScopes = buildMappedScopes;
var _parser = require("../../../workers/parser/index");
var _locColumn = require("./locColumn");
var _rangeMetadata = require("./rangeMetadata");
var _findGeneratedBindingFromPosition = require("./findGeneratedBindingFromPosition");
var _buildGeneratedBindingList = require("./buildGeneratedBindingList");
var _getApplicableBindingsForOriginalPosition = require("./getApplicableBindingsForOriginalPosition");
var _log = require("../../log");
/* 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 <http://mozilla.org/MPL/2.0/>. */
// eslint-disable-next-line max-len
async function buildMappedScopes(source, frame, scopes, sourceMaps, client) {
const originalAstScopes = await (0, _parser.getScopes)(frame.location);
const generatedAstScopes = await (0, _parser.getScopes)(frame.generatedLocation);
if (!originalAstScopes || !generatedAstScopes) {
return null;
}
const originalRanges = await (0, _rangeMetadata.loadRangeMetadata)(source, frame, originalAstScopes, sourceMaps);
if (hasLineMappings(originalRanges)) {
return null;
}
const generatedAstBindings = (0, _buildGeneratedBindingList.buildGeneratedBindingList)(scopes, generatedAstScopes, frame.this);
const {
mappedOriginalScopes,
expressionLookup
} = await mapOriginalBindingsToGenerated(source, originalRanges, originalAstScopes, generatedAstBindings, client, sourceMaps);
const mappedGeneratedScopes = generateClientScope(scopes, mappedOriginalScopes);
return isReliableScope(mappedGeneratedScopes) ? {
mappings: expressionLookup,
scope: mappedGeneratedScopes
} : null;
}
async function mapOriginalBindingsToGenerated(source, originalRanges, originalAstScopes, generatedAstBindings, client, sourceMaps) {
const expressionLookup = {};
const mappedOriginalScopes = [];
const cachedSourceMaps = batchScopeMappings(originalAstScopes, source, sourceMaps);
for (const item of originalAstScopes) {
const generatedBindings = {};
for (const name of Object.keys(item.bindings)) {
const binding = item.bindings[name];
const result = await findGeneratedBinding(cachedSourceMaps, client, source, name, binding, originalRanges, generatedAstBindings);
if (result) {
generatedBindings[name] = result.grip;
if (binding.refs.length !== 0 && // These are assigned depth-first, so we don't want shadowed
// bindings in parent scopes overwriting the expression.
!Object.prototype.hasOwnProperty.call(expressionLookup, name)) {
expressionLookup[name] = result.expression;
}
}
}
mappedOriginalScopes.push({ ...item,
generatedBindings
});
}
return {
mappedOriginalScopes,
expressionLookup
};
}
/**
* Consider a scope and its parents reliable if the vast majority of its
* bindings were successfully mapped to generated scope bindings.
*/
function isReliableScope(scope) {
let totalBindings = 0;
let unknownBindings = 0;
for (let s = scope; s; s = s.parent) {
const vars = s.bindings && s.bindings.variables || {};
for (const key of Object.keys(vars)) {
const binding = vars[key];
totalBindings += 1;
if (binding.value && typeof binding.value === "object" && (binding.value.type === "unscoped" || binding.value.type === "unmapped")) {
unknownBindings += 1;
}
}
} // As determined by fair dice roll.
return totalBindings === 0 || unknownBindings / totalBindings < 0.25;
}
function hasLineMappings(ranges) {
return ranges.every(range => range.columnStart === 0 && range.columnEnd === Infinity);
}
function batchScopeMappings(originalAstScopes, source, sourceMaps) {
const precalculatedRanges = new Map();
const precalculatedLocations = new Map(); // Explicitly dispatch all of the sourcemap requests synchronously up front so
// that they will be batched into a single request for the worker to process.
for (const item of originalAstScopes) {
for (const name of Object.keys(item.bindings)) {
for (const ref of item.bindings[name].refs) {
const locs = [ref];
if (ref.type !== "ref") {
locs.push(ref.declaration);
}
for (const loc of locs) {
precalculatedRanges.set(buildLocationKey(loc.start), sourceMaps.getGeneratedRanges(loc.start, source));
precalculatedLocations.set(buildLocationKey(loc.start), sourceMaps.getGeneratedLocation(loc.start, source));
precalculatedLocations.set(buildLocationKey(loc.end), sourceMaps.getGeneratedLocation(loc.end, source));
}
}
}
}
return {
async getGeneratedRanges(pos, s) {
const key = buildLocationKey(pos);
if (s !== source || !precalculatedRanges.has(key)) {
(0, _log.log)("Bad precalculated mapping");
return sourceMaps.getGeneratedRanges(pos, s);
}
return precalculatedRanges.get(key);
},
async getGeneratedLocation(pos, s) {
const key = buildLocationKey(pos);
if (s !== source || !precalculatedLocations.has(key)) {
(0, _log.log)("Bad precalculated mapping");
return sourceMaps.getGeneratedLocation(pos, s);
}
return precalculatedLocations.get(key);
}
};
}
function buildLocationKey(loc) {
return `${loc.line}:${(0, _locColumn.locColumn)(loc)}`;
}
function generateClientScope(scopes, originalScopes) {
// Pull the root object scope and root lexical scope to reuse them in
// our mapped scopes. This assumes that file file being processed is
// a CommonJS or ES6 module, which might not be ideal. Potentially
let globalLexicalScope = null;
for (let s = scopes; s.parent; s = s.parent) {
// $FlowIgnore - Flow doesn't like casting 'parent'.
globalLexicalScope = s;
}
if (!globalLexicalScope) {
throw new Error("Assertion failure - there should always be a scope");
} // Build a structure similar to the client's linked scope object using
// the original AST scopes, but pulling in the generated bindings
// linked to each scope.
const result = originalScopes.slice(0, -2).reverse().reduce((acc, orig, i) => {
const {
// The 'this' binding data we have is handled independently, so
// the binding data is not included here.
// eslint-disable-next-line no-unused-vars
this: _this,
...variables
} = orig.generatedBindings;
return {
parent: acc,
actor: `originalActor${i}`,
type: orig.type,
bindings: {
arguments: [],
variables
},
...(orig.type === "function" ? {
function: {
displayName: orig.displayName
}
} : null),
...(orig.type === "block" ? {
block: {
displayName: orig.displayName
}
} : null)
};
}, globalLexicalScope); // The rendering logic in getScope 'this' bindings only runs on the current
// selected frame scope, so we pluck out the 'this' binding that was mapped,
// and put it in a special location
const thisScope = originalScopes.find(scope => scope.bindings.this);
if (result.bindings && thisScope) {
result.bindings.this = thisScope.generatedBindings.this || null;
}
return result;
}
function hasValidIdent(range, pos) {
return range.type === "match" || // For declarations, we allow the range on the identifier to be a
// more general "contains" to increase the chances of a match.
pos.type !== "ref" && range.type === "contains";
} // eslint-disable-next-line complexity
async function findGeneratedBinding(sourceMaps, client, source, name, originalBinding, originalRanges, generatedAstBindings) {
// If there are no references to the implicits, then we have no way to
// even attempt to map it back to the original since there is no location
// data to use. Bail out instead of just showing it as unmapped.
if (originalBinding.type === "implicit" && !originalBinding.refs.some(item => item.type === "ref")) {
return null;
}
const loadApplicableBindings = async (pos, locationType) => {
let applicableBindings = await (0, _getApplicableBindingsForOriginalPosition.getApplicableBindingsForOriginalPosition)(generatedAstBindings, source, pos, originalBinding.type, locationType, sourceMaps);
if (applicableBindings.length > 0) {
hadApplicableBindings = true;
}
if (locationType === "ref") {
// Some tooling creates ranges that map a line as a whole, which is useful
// for step-debugging, but can easily lead to finding the wrong binding.
// To avoid these false-positives, we entirely ignore bindings matched
// by ranges that cover full lines.
applicableBindings = applicableBindings.filter(({
range
}) => !(range.start.column === 0 && range.end.column === Infinity));
}
if (locationType !== "ref" && !(await (0, _getApplicableBindingsForOriginalPosition.originalRangeStartsInside)(source, pos, sourceMaps))) {
applicableBindings = [];
}
return applicableBindings;
};
const {
refs
} = originalBinding;
let hadApplicableBindings = false;
let genContent = null;
for (const pos of refs) {
const applicableBindings = await loadApplicableBindings(pos, pos.type);
const range = (0, _rangeMetadata.findMatchingRange)(originalRanges, pos);
if (range && hasValidIdent(range, pos)) {
if (originalBinding.type === "import") {
genContent = await (0, _findGeneratedBindingFromPosition.findGeneratedImportReference)(applicableBindings);
} else {
genContent = await (0, _findGeneratedBindingFromPosition.findGeneratedReference)(applicableBindings);
}
}
if ((pos.type === "class-decl" || pos.type === "class-inner") && source.contentType && source.contentType.match(/\/typescript/)) {
const declRange = (0, _rangeMetadata.findMatchingRange)(originalRanges, pos.declaration);
if (declRange && declRange.type !== "multiple") {
const applicableDeclBindings = await loadApplicableBindings(pos.declaration, pos.type); // Resolve to first binding in the range
const declContent = await (0, _findGeneratedBindingFromPosition.findGeneratedReference)(applicableDeclBindings);
if (declContent) {
// Prefer the declaration mapping in this case because TS sometimes
// maps class declaration names to "export.Foo = Foo;" or to
// the decorator logic itself
genContent = declContent;
}
}
}
if (!genContent && pos.type === "import-decl" && typeof pos.importName === "string") {
const {
importName
} = pos;
const declRange = (0, _rangeMetadata.findMatchingRange)(originalRanges, pos.declaration); // The import declaration should have an original position mapping,
// but otherwise we don't really have preferences on the range type
// because it can have multiple bindings, but we do want to make sure
// that all of the bindings that match the range are part of the same
// import declaration.
if (declRange && declRange.singleDeclaration) {
const applicableDeclBindings = await loadApplicableBindings(pos.declaration, pos.type); // match the import declaration location
genContent = await (0, _findGeneratedBindingFromPosition.findGeneratedImportDeclaration)(applicableDeclBindings, importName);
}
}
if (genContent) {
break;
}
}
if (genContent && genContent.desc) {
return {
grip: genContent.desc,
expression: genContent.expression
};
} else if (genContent) {
// If there is no descriptor for 'this', then this is not the top-level
// 'this' that the server gave us a binding for, and we can just ignore it.
if (name === "this") {
return null;
} // If the location is found but the descriptor is not, then it
// means that the server scope information didn't match the scope
// information from the DevTools parsed scopes.
return {
grip: {
configurable: false,
enumerable: true,
writable: false,
value: {
type: "unscoped",
unscoped: true,
// HACK: Until support for "unscoped" lands in devtools-reps,
// this will make these show as (unavailable).
missingArguments: true
}
},
expression: null
};
} else if (!hadApplicableBindings && name !== "this") {
// If there were no applicable bindings to consider while searching for
// matching bindings, then the source map for this file didn't make any
// attempt to map the binding, and that most likely means that the
// code was entirely emitted from the output code.
return {
grip: {
configurable: false,
enumerable: true,
writable: false,
value: {
type: "null",
optimizedOut: true
}
},
expression: `
(() => {
throw new Error('"' + ${JSON.stringify(name)} + '" has been optimized out.');
})()
`
};
} // If no location mapping is found, then the map is bad, or
// the map is okay but it original location is inside
// of some scope, but the generated location is outside, leading
// us to search for bindings that don't technically exist.
return {
grip: {
configurable: false,
enumerable: true,
writable: false,
value: {
type: "unmapped",
unmapped: true,
// HACK: Until support for "unmapped" lands in devtools-reps,
// this will make these show as (unavailable).
missingArguments: true
}
},
expression: null
};
}