"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = __importDefault(require("debug"));
const typescript_1 = __importDefault(require("./adapter/typescript"));
const espree_1 = __importDefault(require("./adapter/espree"));
const gonzales_pe_1 = __importDefault(require("./adapter/gonzales-pe"));
const strategy_1 = __importDefault(require("./strategy"));
const action_1 = require("./action");
const error_1 = require("./error");
class NodeMutation {
/**
* Configure NodeMutation
* @static
* @param options {Object}
* @param options.strategy {Strategy} - strategy, default is Strategy.THROW_ERROR
* @param options.tabWidth {Number} - tab width, default is 2
*/
static configure(options) {
if (options.strategy) {
this.strategy = options.strategy;
}
if (options.tabWidth) {
this.tabWidth = options.tabWidth;
}
}
/**
* Initialize a NodeMutation
* @param source {string} - file source.
* @param {object} options
* @param {Adapter<T>} options.adapter - adapter to parse the node
*/
constructor(source, { adapter }) {
this.source = source;
this.actions = [];
this.adapter = this.getAdapterInstance(adapter);
}
/**
* Append code to the ast node.
* @param node {T} - ast node
* @param code {string} - new code to append
* @example
* source code of the ast node is
* ```
* class FooBar {
* foo() {}
* }
* ```
* then we call
* ```
* mutation.append(node, "bar() {}");
* ```
* the source code will be rewritten to
* ```
* class FooBar {
* foo() {}
* bar() {}
* }
* ```
*/
append(node, code) {
this.actions.push(new action_1.AppendAction(node, code, { adapter: this.adapter }).process());
}
/**
* Delete source code of the child ast node
* @param node {T} - current ast node
* @param selectors {string|string[]} - selectors to find chid ast nodes
* @param options {DeleteOptions}
* @example
* source code of the ast node is
* ```
* this.foo.bind(this)
* ```
* then we call
* ```
* mutation.delete(["expression.expression.dot", "expression.expression.name", "expression.arguments"])
* ```
* the source code will be rewritten to
* ```
* this.foo
* ```
*/
delete(node, selectors, options = {}) {
this.actions.push(new action_1.DeleteAction(node, selectors, Object.assign(Object.assign({}, options), { adapter: this.adapter })).process());
}
/**
* Group actions, it works both sync and async.
* @param func {Function} - actions in the function will be grouped
*/
group(func) {
return __awaiter(this, void 0, void 0, function* () {
const currentActions = this.actions;
const groupAction = new action_1.GroupAction({ adapter: this.adapter });
this.actions = [];
const result = func.call(this);
if (result instanceof Promise) {
yield result;
}
groupAction.actions = this.actions;
this.actions = currentActions;
this.actions.push(groupAction.process());
});
}
/**
* Insert options
* @typedef {Object} InsertOptions
* @property {string} [at = "end"] - position to insert, "beginning" or "end"
* @property {string} [to] - selector to find the child ast node
*/
/**
* Insert code to the ast node.
* @param node {T} - ast node
* @param code {string} - new code to insert
* @param options {InsertOptions}
* @example
* source code of the ast node is
* ```
* this.foo
* ```
* then we call
* ```
* mutation.insert(node, "::", { at: "beginning" });
* ```
* the source code will be rewritten to
* ```
* ::this.foo
* ```
* if we call
* ```
* mutation.insert(node, ".bar", { to: "expression.expression" })
* ```
* the source code will be rewritten to
* ```
* this.foo.bar
* ```
*/
insert(node, code, options = { at: "end" }) {
this.actions.push(new action_1.InsertAction(node, code, Object.assign(Object.assign({}, options), { adapter: this.adapter })).process());
}
/**
* Indent the ast node.
* @param node {T} - ast node
* @param options {IndentOptions}
* @example
* source code of the ast node is
* ```
* class FooBar {
* }
* ```
* then we call
* ```
* mutation.indent(node, { tabSize: 1 });
* ```
* the source code will be rewritten to
* ```
* class FooBar {
* }
* ```
*/
indent(node, options = { tabSize: 1 }) {
this.actions.push(new action_1.IndentAction(node, Object.assign(Object.assign({}, options), { adapter: this.adapter })).process());
}
/**
* No operation.
* @param node {T} - ast node
*/
noop(node) {
this.actions.push(new action_1.NoopAction(node, { adapter: this.adapter }).process());
}
/**
* Prepend code to the ast node.
* @param node {T} - ast node
* @param code {string} - new code to prepend
* @example
* source code of the ast node is
* ```
* class FooBar {
* foo() {}
* }
* ```
* then we call
* ```
* mutation.prepend(node, "bar() {}");
* ```
* the source code will be rewritten to
* ```
* class FooBar {
* bar() {}
* foo() {}
* }
* ```
*/
prepend(node, code) {
this.actions.push(new action_1.PrependAction(node, code, { adapter: this.adapter }).process());
}
/**
* Remove source code of the ast node
* @param node {T} - ast node
* @param options {RemoveOptions}
* @example
* source code of the ast node is
* ```
* this.foo.bind(this)
* ```
* then we call
* ```
* mutation.remove()
* ```
* the source code will be completely removed
*/
remove(node, options = {}) {
this.actions.push(new action_1.RemoveAction(node, Object.assign(Object.assign({}, options), { adapter: this.adapter })).process());
}
/**
* Replace options
* @typedef {Object} ReplaceOptions
* @property {string} with - new code to replace with
*/
/**
* Replace child node of the ast node with new code
* @param node {T} - current ast node
* @param selectors {string|string[]} - selectors to find chid ast nodes
* @param options {ReplaceOptions}
* @example
* source code of the ast node is
* ```
* class FooBar {}
* ```
* then we call
* ```
* mutation.replace(node, "name", { with: "Synvert" });
* ```
* the source code will be rewritten to
* ```
* class Synvert {}
* ```
*/
replace(node, selectors, options) {
this.actions.push(new action_1.ReplaceAction(node, selectors, Object.assign(Object.assign({}, options), { adapter: this.adapter })).process());
}
/**
* Replace the ast node with new code
* @param node {T} - ast node
* @param code {string} - new code to replace
* @example
* source code of the ast node is
* ```
* !!foobar
* ```
* then we call
* ```
* mutation.replaceWith(node, "Boolean({{expression.operand.operand}})");
* ```
* the source code will be rewritten to
* ```
* Boolean(foobar)
* ```
*/
replaceWith(node, code) {
this.actions.push(new action_1.ReplaceWithAction(node, code, { adapter: this.adapter }).process());
}
/**
* Wrap the ast node with prefix and suffix.
* @param node {T} - ast node
* @param prefix {string} - prefix
* @param suffix {string} - suffix
* @param newLine {boolean} - if true, the prefix and suffix will be wrapped in a new line
* @example
* source code of the ast node is
* ```
* console.log('foo)
* ```
* then we call
* ```
* mutation.wrap(node, { prefix: "function logFoo() {", suffix: "}", newLine: true });
* ```
* the source code will be rewritten to
* ```
* function logFoo() {
* console.log('foo')
* }
* ```
*/
wrap(node, { prefix, suffix, newLine }) {
if (newLine) {
const indentation = this.adapter.getStartLoc(node).column;
this.group(() => {
this.insert(node, prefix + "\n" + (' '.repeat(indentation)), { at: "beginning" });
this.insert(node, "\n" + (' '.repeat(indentation)) + suffix, { at: "end" });
this.indent(node);
});
}
else {
this.group(() => {
this.insert(node, prefix, { at: "beginning" });
this.insert(node, suffix, { at: "end" });
});
}
}
/**
* Process Result
* @typedef {Object} ProcessResult
* @property {boolean} affected - if the source code is affected
* @property {boolean} conflicted - if the actions are conflicted
* @property {string} [newSource] - the new generated source code
*/
/**
* Rewrite the source code based on all actions.
*
* If there's an action range conflict,
* it will raise a ConflictActionError if strategy is set to THROW_ERROR,
* it will process all non conflicted actions and return `{ conflicted: true }`
* @returns {ProcessResult}
*/
process() {
this.actions = this.optimizeGroupActions(this.actions);
const flattenActions = this.flatActions(this.actions);
if (flattenActions.length == 0) {
return { affected: false, conflicted: false };
}
const sortedActions = this.sortFlattenActions(flattenActions);
const conflictActions = this.getConflictActions(sortedActions);
if (conflictActions.length > 0 && this.isStrategy(strategy_1.default.THROW_ERROR)) {
throw new error_1.ConflictActionError();
}
const actions = this.sortFlattenActions(this.flatActions(this.getFilteredActions(conflictActions)));
const newSource = this.rewriteSource(this.source, actions);
return {
affected: true,
conflicted: conflictActions.length !== 0,
newSource
};
}
/**
* Action
* @typedef {Object} Action
* @property {number} start - start position of the action
* @property {number} end - end position of the action
* @property {string} [newCode] - the new generated source code
*/
/**
* Test Result
* @typedef {Object} TestResult
* @property {boolean} affected - if the source code is affected
* @property {boolean} conflicted - if the actions are conflicted
* @property {Action[]} actions - actions to be processed
*/
/**
* Return the actions.
*
* If there's an action range conflict,
* it will raise a ConflictActionError if strategy is set to THROW_ERROR,
* it will process all non conflicted actions and return `{ conflicted: true }`
* @returns {TestResult} if actions are conflicted and the actions
*/
test() {
this.actions = this.optimizeGroupActions(this.actions);
const flattenActions = this.flatActions(this.actions);
if (flattenActions.length == 0) {
return { affected: false, conflicted: false, actions: [] };
}
const sortedActions = this.sortFlattenActions(flattenActions);
const conflictActions = this.getConflictActions(sortedActions);
if (conflictActions.length > 0 && this.isStrategy(strategy_1.default.THROW_ERROR)) {
throw new error_1.ConflictActionError();
}
const actions = this.sortActions(this.getFilteredActions(conflictActions));
return { affected: true, conflicted: conflictActions.length !== 0, actions };
}
/**
* Optimizes a group of actions by recursively optimizing its sub-actions.
* If a group action contains only one action, it replaces the group action with that action.
* If a group action contains more than one action, it optimizes its sub-actions.
* @private
* @param {Action[]} actions
* @returns {Action[]} optimized actions
*/
optimizeGroupActions(actions) {
return actions.flatMap(action => {
if (action.type === 'group') {
// If the group action contains only one action, replace the group action with that action
if (action.actions.length === 1) {
return this.optimizeGroupActions(action.actions);
}
// If the group action contains more than one action, optimize its sub-actions
action.actions = this.optimizeGroupActions(action.actions);
}
return action;
});
}
/**
* It flattens a series of actions by removing any GroupAction objects that contain only a single action. This is done recursively.
* @private
* @param {Action[]} actions
* @returns {Action[]} sorted actions
*/
flatActions(actions) {
const flattenActions = [];
actions.forEach(action => {
if (action.type === "group") {
flattenActions.push(...this.flatActions(action.actions));
}
else {
flattenActions.push(action);
}
});
return flattenActions;
}
/**
* Sort actions by start position and end position.
* @private
* @param {Action[]} flattenActions
* @returns {Action[]} sorted actions
*/
sortFlattenActions(flattenActions) {
return flattenActions.sort(this.compareActions);
}
/**
* Recursively sort actions by start position and end position.
* @private
* @param {Action[]} flattenActions
* @returns {Action[]} sorted actions
*/
sortActions(actions) {
actions.sort(this.compareActions);
actions.forEach(action => {
if (action.type === "group") {
this.sortActions(action.actions);
}
});
return actions;
}
/**
* Action sort function.
* @private
* @param {Action} actionA
* @param {Action} actionB
* @returns {number} returns 1 if actionA goes before actionB, -1 if actionA goes after actionB
*/
compareActions(actionA, actionB) {
if (actionA.start > actionB.start)
return 1;
if (actionA.start < actionB.start)
return -1;
if (actionA.end > actionB.end)
return 1;
if (actionA.end < actionB.end)
return -1;
if (actionA.conflictPosition && actionB.conflictPosition) {
if (actionA.conflictPosition > actionB.conflictPosition)
return 1;
if (actionA.conflictPosition < actionB.conflictPosition)
return -1;
}
return 0;
}
/**
* Rewrite source code with actions.
* @param source {string} source code
* @param actions {Action[]} actions
* @returns {string} new source code
*/
rewriteSource(source, actions) {
actions.reverse().forEach((action) => {
if (action.type === "group") {
source = this.rewriteSource(source, action.actions);
}
else if (typeof action.newCode !== "undefined") {
source =
source.slice(0, action.start) +
action.newCode +
source.slice(action.end);
}
});
return source;
}
/**
* It changes source code from bottom to top, and it can change source code twice at the same time,
* So if there is an overlap between two actions, it removes the conflict actions and operate them in the next loop.
* @private
* @param {Action[]} actions
* @returns {Action[]} conflict actions
*/
getConflictActions(actions) {
let i = actions.length - 1;
let j = i - 1;
const conflictActions = [];
if (i < 0)
return [];
let beginPos = actions[i].start;
let endPos = actions[i].end;
while (j > -1) {
if (beginPos < actions[j].end) {
conflictActions.push(actions[j]);
(0, debug_1.default)("node-mutation")(`Conflict ${actions[j].type}[${actions[j].start}-${actions[j].end}]:${actions[j].newCode}`);
}
else {
i = j;
beginPos = actions[i].start;
endPos = actions[i].end;
}
j--;
}
return conflictActions;
}
/**
* It filters conflict actions from actions.
* @private
* @param {Action[]} conflictActions
* @returns {Action[]} filtered actions
*/
getFilteredActions(conflictActions) {
return this.actions.filter(action => {
if (action.type === 'group') {
// If all child-actions of a group action are conflicted, remove the group action
return action.actions.every(childAction => !conflictActions.includes(childAction));
}
else {
return !conflictActions.includes(action);
}
});
}
isStrategy(strategy) {
return !!NodeMutation.strategy && (NodeMutation.strategy & strategy) === strategy;
}
getAdapterInstance(adapter) {
switch (adapter) {
case "espree":
return new espree_1.default();
case "typescript":
return new typescript_1.default();
case "gonzales-pe":
return new gonzales_pe_1.default();
default:
throw new Error(`Adapter "${adapter}" is not supported.`);
}
}
}
NodeMutation.strategy = strategy_1.default.THROW_ERROR;
NodeMutation.tabWidth = 2;
exports.default = NodeMutation;