Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

[scopes] Batch generated locations #5892

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
398 changes: 20 additions & 378 deletions src/actions/pause/mapScopes.js

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions src/utils/pause/mapScopes/buildGeneratedBindingList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @flow
import { has } from "lodash";
import { locColumn } from "./locColumn";

export function buildGeneratedBindingList(
scopes: Scope,
generatedAstScopes: SourceScope[],
thisBinding: ?BindingContents
): Array<GeneratedBindingLocation> {
// The server's binding data doesn't include general 'this' binding
// information, so we manually inject the one 'this' binding we have into
// the normal binding data we are working with.
const frameThisOwner = generatedAstScopes.find(
generated => "this" in generated.bindings
);

const clientScopes = [];
for (let s = scopes; s; s = s.parent) {
const bindings = s.bindings
? Object.assign({}, ...s.bindings.arguments, s.bindings.variables)
: {};

clientScopes.push(bindings);
}

const generatedMainScopes = generatedAstScopes.slice(0, -2);
const generatedGlobalScopes = generatedAstScopes.slice(-2);

const clientMainScopes = clientScopes.slice(0, generatedMainScopes.length);
const clientGlobalScopes = clientScopes.slice(generatedMainScopes.length);

// Map the main parsed script body using the nesting hierarchy of the
// generated and client scopes.
const generatedBindings = generatedMainScopes.reduce((acc, generated, i) => {
const bindings = clientMainScopes[i];

if (generated === frameThisOwner && thisBinding) {
bindings.this = {
value: thisBinding
};
}

for (const name of Object.keys(generated.bindings)) {
const { refs } = generated.bindings[name];
for (const loc of refs) {
acc.push({
name,
loc,
desc: bindings[name] || null
});
}
}
return acc;
}, []);

// Bindings in the global/lexical global of the generated code may or
// may not be the real global if the generated code is running inside
// of an evaled context. To handle this, we just look up the client scope
// hierarchy to find the closest binding with that name.
for (const generated of generatedGlobalScopes) {
for (const name of Object.keys(generated.bindings)) {
const { refs } = generated.bindings[name];
for (const loc of refs) {
const bindings = clientGlobalScopes.find(b => has(b, name));

if (bindings) {
generatedBindings.push({
name,
loc,
desc: bindings[name]
});
}
}
}
}

// Sort so we can binary-search.
return generatedBindings.sort((a, b) => {
const aStart = a.loc.start;
const bStart = a.loc.start;

if (aStart.line === bStart.line) {
return locColumn(aStart) - locColumn(bStart);
}
return aStart.line - bStart.line;
});
}
110 changes: 110 additions & 0 deletions src/utils/pause/mapScopes/buildMappedScopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// @flow

import { isReliableScope } from "./isReliableScope";
import { buildGeneratedBindingList } from "./buildGeneratedBindingList";
import { findGeneratedBinding } from "./findGeneratedBinding";
import { generateClientScope } from "./generateClientScope";
import { flatMap, uniqBy } from "lodash";
import type {
Frame,
Scope,
Source,
BindingContents,
ScopeBindings
} from "../../../types";

export async function buildMappedScopes(
source: Source,
frame: Frame,
originalAstScopes: Scope,
generatedAstScopes: Scope,
scopes: Scope,
sourceMaps: any,
client: any
): Promise<?{
mappings: {
[string]: string
},
scope: OriginalScope
}> {
const generatedAstBindings = buildGeneratedBindingList(
scopes,
generatedAstScopes,
frame.this
);

const expressionLookup = {};
const mappedOriginalScopes = [];

const generatedLocations = await getGeneratedPositions(
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(
generatedLocations,
client,
name,
binding,
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
});
}

const mappedGeneratedScopes = generateClientScope(
scopes,
mappedOriginalScopes
);

return isReliableScope(mappedGeneratedScopes)
? { mappings: expressionLookup, scope: mappedGeneratedScopes }
: null;
}

async function getGeneratedPositions(originalAstScopes, source, sourceMaps) {
const scopes = Object.values(originalAstScopes);
const bindings = flatMap(scopes, ({ bindings }) => Object.values(bindings));
const refs = flatMap(bindings, ({ refs }) => refs);

const locations = refs.reduce((positions, ref) => {
positions.push(ref.start);
positions.push(ref.end);
if (ref.declaration) {
positions.push(ref.declaration.start);
positions.push(ref.declaration.end);
}
return positions;
}, []);

const originalLocations = uniqBy(locations, loc =>
Object.values(loc).join("-")
);

let sourcesMap = { [source.id]: source.url };
return sourceMaps.batchGeneratedLocations(originalLocations, sourcesMap);
}
104 changes: 104 additions & 0 deletions src/utils/pause/mapScopes/findGeneratedBinding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* 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/>. */

// @flow

// eslint-disable-next-line max-len
import { findGeneratedBindingFromPosition } from "./findGeneratedBindingFromPosition";

import type { GeneratedBindingLocation } from "./types";
import type { BindingData } from "../../workers/parser";

export async function i(
generatedLocations: any,
client: any,
name: string,
originalBinding: BindingData,
generatedAstBindings: Array<GeneratedBindingLocation>
): Promise<?{
grip: BindingContents,
expression: string | null
}> {
// 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 { refs } = originalBinding;

const genContent = await refs.reduce(async (acc, pos) => {
const result = await acc;
if (result) {
return result;
}

return await findGeneratedBindingFromPosition(
generatedLocations,
client,
pos,
name,
originalBinding.type,
generatedAstBindings
);
}, null);

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
};
}

// 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
};
}
35 changes: 21 additions & 14 deletions src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ type GeneratedDescriptor = {
};

export async function findGeneratedBindingFromPosition(
sourceMaps: any,
generatedLocations: any,
client: any,
source: Source,
pos: BindingLocation,
name: string,
type: BindingType,
generatedAstBindings: Array<GeneratedBindingLocation>
): Promise<GeneratedDescriptor | null> {
const range = await getGeneratedLocationRange(pos, source, sourceMaps);
const range = getGeneratedLocationRange(pos, generatedLocations);

if (range) {
const result = await findGeneratedReference(type, generatedAstBindings, {
Expand All @@ -52,10 +51,9 @@ export async function findGeneratedBindingFromPosition(
// If the imported name itself does not map to a useful range, fall back
// to resolving the bindinding using the location of the overall
// import declaration.
importRange = await getGeneratedLocationRange(
importRange = getGeneratedLocationRange(
pos.declaration,
source,
sourceMaps
generatedLocations
);

if (!importRange) {
Expand Down Expand Up @@ -99,7 +97,7 @@ async function findGeneratedReference(

return type === "import"
? await mapImportReferenceToDescriptor(val, mapped)
: await mapBindingReferenceToDescriptor(val, mapped);
: mapBindingReferenceToDescriptor(val, mapped);
}, null);
}

Expand Down Expand Up @@ -130,7 +128,7 @@ async function findGeneratedImportDeclaration(
* Given a generated binding, and a range over the generated code, statically
* check if the given binding matches the range.
*/
async function mapBindingReferenceToDescriptor(
function mapBindingReferenceToDescriptor(
binding: GeneratedBindingLocation,
mapped: {
type: BindingLocationType,
Expand Down Expand Up @@ -341,22 +339,31 @@ function mappingContains(mapped, item) {
);
}

async function getGeneratedLocationRange(
function getGeneratedLocation(pos, generatedLocations) {
const { sourceId, line, column } = pos;
return generatedLocations[sourceId][line][column];
}

function getGeneratedLocationRange(
pos: { start: Location, end: Location },
source: Source,
sourceMaps: any
generatedLocations: any
): Promise<{
start: Location,
end: Location
} | null> {
const start = await sourceMaps.getGeneratedLocation(pos.start, source);
const end = await sourceMaps.getGeneratedLocation(pos.end, source);
// console.log(`> getting locations ${pos.start.line} ${pos.end.line}`);
const start = getGeneratedLocation(pos.start, generatedLocations);
const end = getGeneratedLocation(pos.end, generatedLocations);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to delay this, because this is the logic I'm already going to be changing to updating how the ranges work. Maybe it would make more sense for me to make that a batched lookup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that works!


// Since the map takes the closest location, sometimes mapping a
// binding's location can point at the start of a binding listed after
// it, so we need to make sure it maps to a location that actually has
// a size in order to avoid picking up the wrong descriptor.
if (isEqual(start, end)) {
if (
start.column == end.column &&
start.line == end.line &&
start.sourceId == end.sourceId
) {
return null;
}

Expand Down