/* 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 . */ 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; }