diff options
Diffstat (limited to 'devtools/client/debugger/src/utils/pause/mapScopes/README.md')
-rw-r--r-- | devtools/client/debugger/src/utils/pause/mapScopes/README.md | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/utils/pause/mapScopes/README.md b/devtools/client/debugger/src/utils/pause/mapScopes/README.md new file mode 100644 index 0000000000..2f65b8e847 --- /dev/null +++ b/devtools/client/debugger/src/utils/pause/mapScopes/README.md @@ -0,0 +1,191 @@ +# mapScopes + +The files in this directory manage the Devtools logic for taking the original +JS file from the sourcemap and using the content, combined with source +mappings, to provide an enhanced user experience for debugging the code. + +In this document, we'll refer to the files as either: + +* `original` - The code that the user wrote originally. +* `generated` - The code actually executing in the engine, which may have been + transformed from the `original`, and if a bundler was used, may contain the + final output for several different `original` files. + +The enhancements implemented here break down into as two primary improvements: + +* Rendering the scopes as the user expects them, using the position in the + original file, rather than the content of the generated file. +* Allowing users to interact with the code in the console as if it were the + original code, rather than the generated file. + + +## Overall Approach + +The core goal of scope mapping is to parse the original and generated files, +and then infer a correlation between the bindings in the original file, +and the bindings in the generated file. This is correlation is done via the +source mappings provided alongside the original code in the sourcemap. + +The overall steps break down into: + + +### 1. Parsing + +First the generated and original files are parsed into ASTs, and then the ASTs +are each traversed in order to generate a full picture of the scopes that are +present in the file. This covers information for each binding in each scope, +including all metadata about the declarations themselves, and all references +to each of the bindings. The exact location of each binding reference is the +most important factor because these will be used when working with sourcemaps. + +Importantly, this scope tree's structure also mirrors the structure of the +scope data that the engine itself returns when paused. + + +### 2. Generated Binding -> Grip Correlation + +When the engine pauses, we get back a full description of the current scope, +as well as `grip` objects that can be used as proxy-like objects in order to +access the actual values in the engine itself. + +With this data, along with the location where the engine is paused, we can +use the AST-based scope information from step 1 to attach a line/column +range to each `grip`. Using those mappings we have enough information to get +the real engine value of any variable based on its position in the generated +code, as long as it is in scope at the paused location in the generated file. + +The generated and engine scopes are correlated depth-first, because in some +cases the scope data from the engine will be deeper that the data from the +parsed code, meaning there are additional unknown parent scopes. This can +happen, for instance, if the executing code is running inside of an `eval` +since the parser only knows about the scopes inside of `eval`, and cannot +know what context it is executed inside of. + + +### 3. Original Binding -> Generated Binding Correlation + +Now that we can get the value of a generated location, we need to decide which +generated location correlates with which original location. For each of +the original bindings in each original scope, we iterate through the +individual references to the binding in the file and: + +1. Use the sourcemap to convert the original location into a range on the + generated code. +2. Filter the available bindings in the generated file down to those that + overlap with this generated range. +3. Perform heuristics to decide if any of the bindings in the range appear + to be a valid and safe mapping. + +These steps allow us to build a datastructure that describes the scope +as it is declared in the original file, while rendering the value using the +`grip` objects, as returned from the engine itself. + +There is additional complexity here in the exact details of the heuristics +mentioned above. See the later Heuristics sections diving into these details. + +During this phase, one additional task is performed, which is the construction +of expressions that relate back to the original binding. After matching each +binding in a range, we know the name of the binding in the original scope, +and we _also_ know the name of the generated binding, or more generally the +expression to evaluate in order to get the value of the original binding. + +These expression values are important for ensuring that the developer console +is able to allow users to interact with the original code. If a user types +a variable into the console, it can be translated into the expression +correlated with that original variable, allowing the code to behave as it +would have, were it actually part of the original file. + + +### 4. Generate a Scope Tree + +The structure generated in step 3 is converted into a structure compatible +with the standard scope data returned from the engine itself in order to allow +for consistent usage of scope data, irrespective of the scope data source. +This stage also re-attaches the global scope data, which is otherwise ignored +during the correlation process. + + +### 5. Validation + +As a simple form of validation, we ensure that a large percentage of bindings +were actually resolved to a `grip` object. This offers some amount of +protection, if the sourcemap itself turned out to be somewhat low-quality. + +This happens often with tooling that generates maps that simply map an original +line to a generated line. While line-to-line mappings still enable step +debugging, they do not provide enough information for original-scope mapping. + +On the other hand, generally line-to-line mappings are usually generated by +tooling that performs minimal transformations on their own, so it is often +acceptable to fall back to the engine-only generated-file scope data in this +case. + + +## Range Mapping Heuristics + +Since we know lots of information about the original bindings when performing +matches, it is possible to make educated guesses about how their generated +code will have likely been transformed. This allows us to perform more +aggressive matching logic what would normally be too false-positive-heavy +for generic use, when it is deemed safe enough. + + +### Standard Matching + +In the general case, we iterate through the bindings that were found in the +generated range, and consider it a match if the binding overlaps with the +start of the range. One extra case here is that ranges at the start of +a line often point to the first binding in a range and do not overlap the +first binding, so there is special-casing for the first binding in each range. + + +### ES6 Import Reference Special Case + +References to ES6 imports are often transformed into property accesses on objects +in order to emulate the "live-binding" behavior defined by ES6. The standard +logic here would end up always resolving the imported value to be the import +namespace object itself, rather than reading the value of the property. + +To support this, specifically for ES6 imports, we walk outward from the matched +binding itself, taking property accesses into account, as long as the +property access itself is also within the mapped range. + +While decently effective, there are currently two downsides to this approach: + +* The "live" behavior of imports is often implemented using accessor + properties, which as of the time of this writing, cannot be evaluated to + retrieve their real value. +* The "live" behavior of imports is sometimes implemented with function calls, + which also also cannot be evaluated, causing their value to be + unknown. + + +### ES6 Import Declaration Special Case + +If there are no references to an imported value, or matching based on the +reference failed, we fall back to a second case. + +ES6 import declarations themselves often map back to the location of the +declaration of the imported module's namespace object. By getting the range for +the import declaration itself, we can infer which generated binding is the +namespace object. Alongside that, we already know the name of the property on +the namespace itself because it is statically knowable by analyzing the +import declaration. + +By combining both of those pieces of information, we can access the namespace's +property to get the imported value. + + +### Typescript Classes + +Typescript have several non-ideal ways in which they output source maps, most +of which center around classes that are either exported, or decorated. These +issues are currently tracked in [Typescript issue #22833][1]. + +To work around this issue, we use a method similar to the import declaration +case above. While the class name itself often maps to unhelpful locations, +the class declaration itself generally maps to the class's transformed binding, +so we make use of the class declaration location instead of the location of +the class's declared name in these cases. + + [1]: https://github.com/Microsoft/TypeScript/issues/22833 |