@quick-game/cli
Version:
Command line interface for rapid qg development
94 lines • 3.92 kB
JavaScript
// 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 { ScopeVariableAnalysis } from './ScopeParser.js';
export function substituteExpression(expression, nameMap) {
const replacements = computeSubstitution(expression, nameMap);
return applySubstitution(expression, replacements);
}
// 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, nameMap) {
// Parse the expression and find variables and scopes.
const root = Acorn.parse(expression, { ecmaVersion: ECMA_VERSION, allowAwaitOutsideFunction: true, ranges: false, checkPrivateFields: false });
const scopeVariables = new ScopeVariableAnalysis(root);
scopeVariables.run();
const freeVariables = scopeVariables.getFreeVariables();
const result = [];
// Prepare the machinery for generating fresh names (to avoid variable captures).
const allNames = scopeVariables.getAllNames();
for (const rename of nameMap.values()) {
if (rename) {
allNames.add(rename);
}
}
function getNewName(base) {
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;
}
if (rename === null) {
throw new Error(`Cannot substitute '${name}' as the underlying variable '${rename}' is unavailable`);
}
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 === 3 /* 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, replacements) {
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('');
}
//# sourceMappingURL=Substitute.js.map