UNPKG

chrome-devtools-frontend

Version:
106 lines (96 loc) 3.68 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Acorn from '../../third_party/acorn/acorn.js'; import {ECMA_VERSION} from './AcornTokenizer.js'; import {DefinitionKind} from './FormatterActions.js'; import {ScopeVariableAnalysis} from './ScopeParser.js'; export function substituteExpression(expression: string, nameMap: Map<string, string>): string { const replacements = computeSubstitution(expression, nameMap); return applySubstitution(expression, replacements); } interface Replacement { from: string; to: string; offset: number; isShorthandAssignmentProperty: boolean; } // Given an |expression| and a mapping from names to new names, the |computeSubstitution| // function returns a list of replacements sorted by the offset. The function throws if // it cannot parse the expression or the substitution is impossible to perform (for example // if the substitution target is 'this' within a function, it would become bound there). function computeSubstitution(expression: string, nameMap: Map<string, string>): Replacement[] { // Parse the expression and find variables and scopes. const root = Acorn.parse(expression, {ecmaVersion: ECMA_VERSION, allowAwaitOutsideFunction: true, ranges: false}) as Acorn.ESTree.Node; const scopeVariables = new ScopeVariableAnalysis(root); scopeVariables.run(); const freeVariables = scopeVariables.getFreeVariables(); const result: Replacement[] = []; // Prepare the machinery for generating fresh names (to avoid variable captures). const allNames = scopeVariables.getAllNames(); for (const rename of nameMap.values()) { allNames.add(rename); } function getNewName(base: string): string { let i = 1; while (allNames.has(`${base}_${i}`)) { i++; } const newName = `${base}_${i}`; allNames.add(newName); return newName; } // Perform the substitutions. for (const [name, rename] of nameMap.entries()) { const defUse = freeVariables.get(name); if (!defUse) { continue; } const binders = []; for (const use of defUse) { result.push({ from: name, to: rename, offset: use.offset, isShorthandAssignmentProperty: use.isShorthandAssignmentProperty, }); binders.push(...use.scope.findBinders(rename)); } // If there is a capturing binder, rename the bound variable. for (const binder of binders) { if (binder.definitionKind === DefinitionKind.Fixed) { // If the identifier is bound to a fixed name, such as 'this', // then refuse to do the substitution. throw new Error(`Cannot avoid capture of '${rename}'`); } const newName = getNewName(rename); for (const use of binder.uses) { result.push({ from: rename, to: newName, offset: use.offset, isShorthandAssignmentProperty: use.isShorthandAssignmentProperty, }); } } } result.sort((l, r) => l.offset - r.offset); return result; } function applySubstitution(expression: string, replacements: Replacement[]): string { const accumulator = []; let last = 0; for (const r of replacements) { accumulator.push(expression.slice(last, r.offset)); let replacement = r.to; if (r.isShorthandAssignmentProperty) { // Let us expand the shorthand to full assignment. replacement = `${r.from}: ${r.to}`; } accumulator.push(replacement); last = r.offset + r.from.length; } accumulator.push(expression.slice(last)); return accumulator.join(''); }