UNPKG

antlr4-runtime

Version:

JavaScript runtime for ANTLR4

443 lines (405 loc) 14.8 kB
import Token from "./Token.js"; import Interval from "./misc/Interval.js"; /** * @typedef {import("./CommonTokenStream").default} CommonTokenStream * @typedef {Array<RewriteOperation | undefined>} Rewrites * @typedef {unknown} Text */ export default class TokenStreamRewriter { // eslint-disable-next-line no-undef static DEFAULT_PROGRAM_NAME = "default"; /** * @param {CommonTokenStream} tokens The token stream to modify */ constructor(tokens) { this.tokens = tokens; /** @type {Map<string, Rewrites>} */ this.programs = new Map(); } /** * @returns {CommonTokenStream} */ getTokenStream() { return this.tokens; } /** * Insert the supplied text after the specified token (or token index) * @param {Token | number} tokenOrIndex * @param {Text} text * @param {string} [programName] */ insertAfter(tokenOrIndex, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) { /** @type {number} */ let index; if (typeof tokenOrIndex === "number") { index = tokenOrIndex; } else { index = tokenOrIndex.tokenIndex; } // to insert after, just insert before next index (even if past end) let rewrites = this.getProgram(programName); let op = new InsertAfterOp(this.tokens, index, rewrites.length, text); rewrites.push(op); } /** * Insert the supplied text before the specified token (or token index) * @param {Token | number} tokenOrIndex * @param {Text} text * @param {string} [programName] */ insertBefore(tokenOrIndex, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) { /** @type {number} */ let index; if (typeof tokenOrIndex === "number") { index = tokenOrIndex; } else { index = tokenOrIndex.tokenIndex; } const rewrites = this.getProgram(programName); const op = new InsertBeforeOp(this.tokens, index, rewrites.length, text); rewrites.push(op); } /** * Replace the specified token with the supplied text * @param {Token | number} tokenOrIndex * @param {Text} text * @param {string} [programName] */ replaceSingle(tokenOrIndex, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) { this.replace(tokenOrIndex, tokenOrIndex, text, programName); } /** * Replace the specified range of tokens with the supplied text * @param {Token | number} from * @param {Token | number} to * @param {Text} text * @param {string} [programName] */ replace(from, to, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) { if (typeof from !== "number") { from = from.tokenIndex; } if (typeof to !== "number") { to = to.tokenIndex; } if (from > to || from < 0 || to < 0 || to >= this.tokens.size) { throw new RangeError(`replace: range invalid: ${from}..${to}(size=${this.tokens.size})`); } let rewrites = this.getProgram(programName); let op = new ReplaceOp(this.tokens, from, to, rewrites.length, text); rewrites.push(op); } /** * Delete the specified range of tokens * @param {number | Token} from * @param {number | Token} to * @param {string} [programName] */ delete(from, to, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) { if (typeof to === "undefined") { to = from; } this.replace(from, to, null, programName); } /** * @param {string} name * @returns {Rewrites} */ getProgram(name) { let is = this.programs.get(name); if (is == null) { is = this.initializeProgram(name); } return is; } /** * @param {string} name * @returns {Rewrites} */ initializeProgram(name) { const is = []; this.programs.set(name, is); return is; } /** * Return the text from the original tokens altered per the instructions given to this rewriter * @param {Interval | string} [intervalOrProgram] * @param {string} [programName] * @returns {string} */ getText(intervalOrProgram, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) { let interval; if (intervalOrProgram instanceof Interval) { interval = intervalOrProgram; } else { interval = new Interval(0, this.tokens.size - 1); } if (typeof intervalOrProgram === "string") { programName = intervalOrProgram; } const rewrites = this.programs.get(programName); let start = interval.start; let stop = interval.stop; // ensure start/end are in range if (stop > this.tokens.size - 1) { stop = this.tokens.size - 1; } if (start < 0) { start = 0; } if (rewrites == null || rewrites.length === 0) { return this.tokens.getText(new Interval(start, stop)); // no instructions to execute } let buf = []; // First, optimize instruction stream let indexToOp = this.reduceToSingleOperationPerIndex(rewrites); // Walk buffer, executing instructions and emitting tokens let i = start; while (i <= stop && i < this.tokens.size) { let op = indexToOp.get(i); indexToOp.delete(i); // remove so any left have index size-1 let t = this.tokens.get(i); if (op == null) { // no operation at that index, just dump token if (t.type !== Token.EOF) { buf.push(String(t.text)); } i++; // move to next token } else { i = op.execute(buf); // execute operation and skip } } // include stuff after end if it's last index in buffer // So, if they did an insertAfter(lastValidIndex, "foo"), include // foo if end==lastValidIndex. if (stop === this.tokens.size - 1) { // Scan any remaining operations after last token // should be included (they will be inserts). for (const op of indexToOp.values()) { if (op.index >= this.tokens.size - 1) { buf.push(op.text.toString()); } } } return buf.join(""); } /** * @param {Rewrites} rewrites * @returns {Map<number, RewriteOperation>} a map from token index to operation */ reduceToSingleOperationPerIndex(rewrites) { // WALK REPLACES for (let i = 0; i < rewrites.length; i++) { let op = rewrites[i]; if (op == null) { continue; } if (!(op instanceof ReplaceOp)) { continue; } let rop = op; // Wipe prior inserts within range let inserts = this.getKindOfOps(rewrites, InsertBeforeOp, i); for (let iop of inserts) { if (iop.index === rop.index) { // E.g., insert before 2, delete 2..2; update replace // text to include insert before, kill insert rewrites[iop.instructionIndex] = undefined; rop.text = iop.text.toString() + (rop.text != null ? rop.text.toString() : ""); } else if (iop.index > rop.index && iop.index <= rop.lastIndex) { // delete insert as it's a no-op. rewrites[iop.instructionIndex] = undefined; } } // Drop any prior replaces contained within let prevReplaces = this.getKindOfOps(rewrites, ReplaceOp, i); for (let prevRop of prevReplaces) { if (prevRop.index >= rop.index && prevRop.lastIndex <= rop.lastIndex) { // delete replace as it's a no-op. rewrites[prevRop.instructionIndex] = undefined; continue; } // throw exception unless disjoint or identical let disjoint = prevRop.lastIndex < rop.index || prevRop.index > rop.lastIndex; // Delete special case of replace (text==null): // D.i-j.u D.x-y.v | boundaries overlap combine to max(min)..max(right) if (prevRop.text == null && rop.text == null && !disjoint) { rewrites[prevRop.instructionIndex] = undefined; // kill first delete rop.index = Math.min(prevRop.index, rop.index); rop.lastIndex = Math.max(prevRop.lastIndex, rop.lastIndex); } else if (!disjoint) { throw new Error(`replace op boundaries of ${rop} overlap with previous ${prevRop}`); } } } // WALK INSERTS for (let i = 0; i < rewrites.length; i++) { let op = rewrites[i]; if (op == null) { continue; } if (!(op instanceof InsertBeforeOp)) { continue; } let iop = op; // combine current insert with prior if any at same index let prevInserts = this.getKindOfOps(rewrites, InsertBeforeOp, i); for (let prevIop of prevInserts) { if (prevIop.index === iop.index) { if (prevIop instanceof InsertAfterOp) { iop.text = this.catOpText(prevIop.text, iop.text); rewrites[prevIop.instructionIndex] = undefined; } else if (prevIop instanceof InsertBeforeOp) { // combine objects // convert to strings...we're in process of toString'ing // whole token buffer so no lazy eval issue with any templates iop.text = this.catOpText(iop.text, prevIop.text); // delete redundant prior insert rewrites[prevIop.instructionIndex] = undefined; } } } // look for replaces where iop.index is in range; error let prevReplaces = this.getKindOfOps(rewrites, ReplaceOp, i); for (let rop of prevReplaces) { if (iop.index === rop.index) { rop.text = this.catOpText(iop.text, rop.text); rewrites[i] = undefined; // delete current insert continue; } if (iop.index >= rop.index && iop.index <= rop.lastIndex) { throw new Error(`insert op ${iop} within boundaries of previous ${rop}`); } } } /** @type {Map<number, RewriteOperation>} */ let m = new Map(); for (let op of rewrites) { if (op == null) { // ignore deleted ops continue; } if (m.get(op.index) != null) { throw new Error("should only be one op per index"); } m.set(op.index, op); } return m; } /** * @param {Text} a * @param {Text} b * @returns {string} */ catOpText(a, b) { let x = ""; let y = ""; if (a != null) { x = a.toString(); } if (b != null) { y = b.toString(); } return x + y; } /** * Get all operations before an index of a particular kind * @param {Rewrites} rewrites * @param {any} kind * @param {number} before */ getKindOfOps(rewrites, kind, before) { return rewrites.slice(0, before).filter(op => op && op instanceof kind); } } class RewriteOperation { /** * @param {CommonTokenStream} tokens * @param {number} index * @param {number} instructionIndex * @param {Text} text */ constructor(tokens, index, instructionIndex, text) { this.tokens = tokens; this.instructionIndex = instructionIndex; this.index = index; this.text = text === undefined ? "" : text; } toString() { let opName = this.constructor.name; const $index = opName.indexOf("$"); opName = opName.substring($index + 1, opName.length); return "<" + opName + "@" + this.tokens.get(this.index) + ":\"" + this.text + "\">"; } } class InsertBeforeOp extends RewriteOperation { /** * @param {CommonTokenStream} tokens * @param {number} index * @param {number} instructionIndex * @param {Text} text */ constructor(tokens, index, instructionIndex, text) { super(tokens, index, instructionIndex, text); } /** * @param {string[]} buf * @returns {number} the index of the next token to operate on */ execute(buf) { if (this.text) { buf.push(this.text.toString()); } if (this.tokens.get(this.index).type !== Token.EOF) { buf.push(String(this.tokens.get(this.index).text)); } return this.index + 1; } } class InsertAfterOp extends InsertBeforeOp { /** * @param {CommonTokenStream} tokens * @param {number} index * @param {number} instructionIndex * @param {Text} text */ constructor(tokens, index, instructionIndex, text) { super(tokens, index + 1, instructionIndex, text); // insert after is insert before index+1 } } class ReplaceOp extends RewriteOperation { /** * @param {CommonTokenStream} tokens * @param {number} from * @param {number} to * @param {number} instructionIndex * @param {Text} text */ constructor(tokens, from, to, instructionIndex, text) { super(tokens, from, instructionIndex, text); this.lastIndex = to; } /** * @param {string[]} buf * @returns {number} the index of the next token to operate on */ execute(buf) { if (this.text) { buf.push(this.text.toString()); } return this.lastIndex + 1; } toString() { if (this.text == null) { return "<DeleteOp@" + this.tokens.get(this.index) + ".." + this.tokens.get(this.lastIndex) + ">"; } return "<ReplaceOp@" + this.tokens.get(this.index) + ".." + this.tokens.get(this.lastIndex) + ":\"" + this.text + "\">"; } }