Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import { locColumn } from "./locColumn";
import { mappingContains } from "./mappingContains";
// eslint-disable-next-line max-len
import { clientCommands } from "../../../client/firefox";
/**
* Given a mapped range over the generated source, attempt to resolve a real
* binding descriptor that can be used to access the value.
*/
export async function findGeneratedReference(applicableBindings) {
// We can adjust this number as we go, but these are a decent start as a
// general heuristic to assume the bindings were bad or just map a chunk of
// whole line or something.
if (applicableBindings.length > 4) {
// Babel's for..of generates at least 3 bindings inside one range for
// block-scoped loop variables, so we shouldn't go below that.
applicableBindings = [];
}
for (const applicable of applicableBindings) {
const result = await mapBindingReferenceToDescriptor(applicable);
if (result) {
return result;
}
}
return null;
}
export async function findGeneratedImportReference(applicableBindings) {
// When wrapped, for instance as `Object(ns.default)`, the `Object` binding
// will be the first in the list. To avoid resolving `Object` as the
// value of the import itself, we potentially skip the first binding.
applicableBindings = applicableBindings.filter((applicable, i) => {
if (
!applicable.firstInRange ||
applicable.binding.loc.type !== "ref" ||
applicable.binding.loc.meta
) {
return true;
}
const next =
i + 1 < applicableBindings.length ? applicableBindings[i + 1] : null;
return !next || next.binding.loc.type !== "ref" || !next.binding.loc.meta;
});
// We can adjust this number as we go, but these are a decent start as a
// general heuristic to assume the bindings were bad or just map a chunk of
// whole line or something.
if (applicableBindings.length > 2) {
// Babel's for..of generates at least 3 bindings inside one range for
// block-scoped loop variables, so we shouldn't go below that.
applicableBindings = [];
}
for (const applicable of applicableBindings) {
const result = await mapImportReferenceToDescriptor(applicable);
if (result) {
return result;
}
}
return null;
}
/**
* Given a mapped range over the generated source and the name of the imported
* value that is referenced, attempt to resolve a binding descriptor for
* the import's value.
*/
export async function findGeneratedImportDeclaration(
applicableBindings,
importName
) {
// We can adjust this number as we go, but these are a decent start as a
// general heuristic to assume the bindings were bad or just map a chunk of
// whole line or something.
if (applicableBindings.length > 10) {
// Import declarations tend to have a large number of bindings for
// for things like 'require' and 'interop', so this number is larger
// than other binding count checks.
applicableBindings = [];
}
let result = null;
for (const { binding } of applicableBindings) {
if (binding.loc.type === "ref") {
continue;
}
const namespaceDesc = await binding.desc();
if (isPrimitiveValue(namespaceDesc)) {
continue;
}
if (!isObjectValue(namespaceDesc)) {
// We want to handle cases like
//
// var _mod = require(...);
// var _mod2 = _interopRequire(_mod);
//
// where "_mod" is optimized out because it is only referenced once. To
// allow that, we track the optimized-out value as a possible result,
// but allow later binding values to overwrite the result.
result = {
name: binding.name,
desc: namespaceDesc,
expression: binding.name,
};
continue;
}
const desc = await readDescriptorProperty(namespaceDesc, importName);
const expression = `${binding.name}.${importName}`;
if (desc) {
result = {
name: binding.name,
desc,
expression,
};
break;
}
}
return result;
}
/**
* Given a generated binding, and a range over the generated code, statically
* check if the given binding matches the range.
*/
async function mapBindingReferenceToDescriptor({
binding,
range,
firstInRange,
firstOnLine,
}) {
// Allow the mapping to point anywhere within the generated binding
// location to allow for less than perfect sourcemaps. Since you also
// need at least one character between identifiers, we also give one
// characters of space at the front the generated binding in order
// to increase the probability of finding the right mapping.
if (
range.start.line === binding.loc.start.line &&
// If a binding is the first on a line, Babel will extend the mapping to
// include the whitespace between the newline and the binding. To handle
// that, we skip the range requirement for starting location.
(firstInRange ||
firstOnLine ||
locColumn(range.start) >= locColumn(binding.loc.start)) &&
locColumn(range.start) <= locColumn(binding.loc.end)
) {
return {
name: binding.name,
desc: await binding.desc(),
expression: binding.name,
};
}
return null;
}
/**
* Given an generated binding, and a range over the generated code, statically
* evaluate accessed properties within the mapped range to resolve the actual
* imported value.
*/
async function mapImportReferenceToDescriptor({ binding, range }) {
if (binding.loc.type !== "ref") {
return null;
}
// Expression matches require broader searching because sourcemaps usage
// varies in how they map certain things. For instance given
//
// import { bar } from "mod";
// bar();
//
// The "bar()" expression is generally expanded into one of two possibly
// forms, both of which map the "bar" identifier in different ways. See
// the "^^" markers below for the ranges.
//
// (0, foo.bar)() // Babel
// ^^^^^^^ // mapping
// ^^^ // binding
// vs
//
// __webpack_require__.i(foo.bar)() // Webpack 2
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // mapping
// ^^^ // binding
// vs
//
// Object(foo.bar)() // Webpack >= 3
// ^^^^^^^^^^^^^^^ // mapping
// ^^^ // binding
//
// Unfortunately, Webpack also has a tendancy to over-map past the call
// expression to the start of the next line, at least when there isn't
// anything else on that line that is mapped, e.g.
//
// Object(foo.bar)()
// ^^^^^^^^^^^^^^^^^
// ^ // wrapped to column 0 of next line
if (!mappingContains(range, binding.loc)) {
return null;
}
// Webpack 2's import declarations wrap calls with an identity fn, so we
// need to make sure to skip that binding because it is mapped to the
// location of the original binding usage.
if (
binding.name === "__webpack_require__" &&
binding.loc.meta &&
binding.loc.meta.type === "member" &&
binding.loc.meta.property === "i"
) {
return null;
}
let expression = binding.name;
let desc = await binding.desc();
if (binding.loc.type === "ref") {
const { meta } = binding.loc;
// Limit to 2 simple property or inherits operartions, since it would
// just be more work to search more and it is very unlikely that
// bindings would be mapped to more than a single member + inherits
// wrapper.
for (
let op = meta, index = 0;
op && mappingContains(range, op) && desc && index < 2;
index++, op = op?.parent
) {
// Calling could potentially trigger side-effects, which would not
// be ideal for this case.
if (op.type === "call") {
return null;
}
if (op.type === "inherit") {
continue;
}
desc = await readDescriptorProperty(desc, op.property);
expression += `.${op.property}`;
}
}
return desc
? {
name: binding.name,
desc,
expression,
}
: null;
}
function isPrimitiveValue(desc) {
return desc && (!desc.value || typeof desc.value !== "object");
}
function isObjectValue(desc) {
return (
desc &&
!isPrimitiveValue(desc) &&
desc.value.type === "object" &&
// Note: The check for `.type` might already cover the optimizedOut case
// but not 100% sure, so just being cautious.
!desc.value.optimizedOut
);
}
async function readDescriptorProperty(desc, property) {
if (!desc) {
return null;
}
if (typeof desc.value !== "object" || !desc.value) {
// If accessing a property on a primitive type, just return 'undefined'
// as the value.
return {
value: {
type: "undefined",
},
};
}
if (!isObjectValue(desc)) {
// If we got a non-primitive descriptor but it isn't an object, then
// it's definitely not the namespace and it is probably an error.
return desc;
}
const objectFront = clientCommands.createObjectFront(desc.value);
return (await objectFront.getProperty(property)).descriptor;
}