action/base.js

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseAction = void 0;
const debug_1 = __importDefault(require("debug"));
const error_1 = require("../error");
/**
 * Action does some real actions, e.g. insert / replace / delete code.
 */
class BaseAction {
    /**
     * Create an Action.
     * @param {T} node
     * @param {string} code - new code to insert, replace or delete
     * @param {object} options
     * @param {Adapter<T>} options.adapter - adapter to parse the node
     */
    constructor(node, code, { adapter }) {
        this.node = node;
        this.code = code;
        this.start = -1;
        this.end = -1;
        this.type = "";
        this.adapter = adapter;
    }
    /**
     * Calculate begin and end positions, and return an action.
     * @returns {Action} action
     */
    process() {
        this.calculatePositions();
        (0, debug_1.default)("node-mutation")(`Process ${this.type}[${this.start}-${this.end}]:${this.newCode}`);
        const result = {
            type: this.type,
            start: this.start,
            end: this.end,
            newCode: this.newCode,
            actions: this.actions,
        };
        if (this.conflictPosition) {
            result.conflictPosition = this.conflictPosition;
        }
        return result;
    }
    /**
     * Get the new source code after evaluating the node.
     * @returns {string} The new source code.
     * @example
     * this.node = ts.createSourceFile("code.ts", "foo.substring(1, 2)")
     * this.code = "{{expression.name}}"
     * rewrittenSource() // substring
     *
     * // node array
     * const node = ts.createSourceFile("code.ts", "foo.substring(1, 2)")
     * rewrittenSource(node, "{{expression.expression.expression}}.slice({{expression.arguments}})") // foo.slice(1, 2)
     *
     * // index for node array
     * const node = ts.createSourceFile("code.ts", "foo.substring(1, 2)")
     * rewrittenSource(node, "{{expression.arguments.1}}") // 2
     *
     * // {name}Property for node who has properties
     * const node = ts.createSourceFile("code.ts", "const foobar = { foo: 'foo', bar: 'bar' }")
     * rewritten_source(node, '{{declarationList.declarations.0.initializer.fooProperty}}')) # foo: 'foo'
     *
     * // {name}Initializer for node who has properties
     * const node = ts.createSourceFile("code.ts", "const foobar = { foo: 'foo', bar: 'bar' }")
     * rewritten_source(node, '{{declarationList.declarations.0.initializer.fooInitializer}}')) # 'foo'
     */
    rewrittenSource() {
        return this.code.replace(/{{(.+?)}}/gm, (_string, match, _offset) => {
            if (!match)
                return null;
            const obj = this.adapter.childNodeValue(this.node, match);
            if (obj) {
                if (Array.isArray(obj)) {
                    return this.adapter.fileContent(this.node).slice(this.adapter.getStart(obj[0]), this.adapter.getEnd(obj[obj.length - 1]));
                }
                if (obj.hasOwnProperty("kind") || obj.hasOwnProperty("type")) {
                    return this.adapter.getSource(obj);
                }
                else {
                    return obj;
                }
            }
            else {
                throw new error_1.NotSupportedError(`can not parse "${this.code}"`);
            }
        });
    }
    /**
     * Get the source code of this node.
     * @protected
     * @returns source code of this node.
     */
    source() {
        return this.adapter.fileContent(this.node);
    }
    /**
     * Squeeze spaces from source code.
     * @protected
     */
    squeezeSpaces() {
        const source = this.source();
        const beforeCharIsSpace = source[this.start - 1] === " ";
        const afterCharIsSpace = source[this.end] == " ";
        if (beforeCharIsSpace && afterCharIsSpace) {
            this.start = this.start - 1;
        }
    }
    /**
     * Squeeze empty lines from source code.
     * @protected
     */
    squeezeLines() {
        const lines = this.source().split("\n");
        const beginLine = this.adapter.getStartLoc(this.node).line;
        const endLine = this.adapter.getEndLoc(this.node).line;
        const beforeLineIsBlank = endLine === 1 || lines[beginLine - 2] === "";
        const afterLineIsBlank = lines[endLine] === "";
        if (lines.length > 1 && beforeLineIsBlank && afterLineIsBlank) {
            this.end = this.end + "\n".length;
        }
    }
    /**
     * Remove unused comma.
     * e.g. `foobar(foo, bar)`, if we remove `foo`, the comma should also be removed,
     * the code should be changed to `foobar(bar)`.
     * @protected
     */
    removeComma() {
        let leadingCount = 1;
        while (true) {
            if (this.source()[this.start - leadingCount] === ',') {
                this.start -= leadingCount;
                return;
            }
            else if (['\n', '\r', '\t', ' '].includes(this.source()[this.start - leadingCount])) {
                leadingCount += 1;
            }
            else {
                break;
            }
        }
        let trailingCount = 0;
        while (true) {
            if (this.source()[this.end + trailingCount] === ',') {
                this.end += trailingCount + 1;
                return;
            }
            else if (this.source()[this.end + trailingCount] === ' ') {
                trailingCount += 1;
            }
            else {
                break;
            }
        }
    }
    /**
     * Remove unused space.
     * e.g. `<div foo='bar'>foobar</div>`, if we remove `foo='bar`, the space should also be removed,
     * the code should be changed to `<div>foobar</div>`.
     * @protected
     */
    removeSpace() {
        // this happens when removing a property in jsx element.
        if (this.prevTokenIs(" ") && this.nextTokenIs(">")) {
            this.start = this.start - 1;
        }
        if (this.prevTokenIs(" ") && this.nextTokenIs("\n")) {
            this.start = this.start - 1;
        }
    }
    /**
     * Add indent to code.
     * @param code {string} - code to add indent
     * @param indent {number} - indent level
     * @returns {string} - code with indent
     */
    addIndentToCode(code, indent) {
        const spaces = " ".repeat(indent);
        return code.split("\n").map((line) => line.length > 0 ? spaces + line : line).join("\n") + "\n";
    }
    /**
     * Check if next token is substr.
     * @private
     * @param {string} substr
     * @returns {boolean} true if next token is equal to substr
     */
    nextTokenIs(substr) {
        return (this.source().slice(this.end, this.end + substr.length) === substr);
    }
    /**
     * Check if previous token is substr.
     * @private
     * @param {string} substr
     * @returns {boolean} true if previous token is equal to substr
     */
    prevTokenIs(substr) {
        return (this.source().slice(this.start - substr.length, this.start) ===
            substr);
    }
}
exports.BaseAction = BaseAction;