"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FunctionParser = exports.dedentFunction = exports.functionToString = exports.USED_METHOD_KEY = void 0; const quote_1 = require("./quote"); /** * Used in function stringification. */ /* istanbul ignore next */ const METHOD_NAMES_ARE_QUOTED = { " "() { /* Empty. */ }, }[" "] .toString() .charAt(0) === '"'; const FUNCTION_PREFIXES = { Function: "function ", GeneratorFunction: "function* ", AsyncFunction: "async function ", AsyncGeneratorFunction: "async function* ", }; const METHOD_PREFIXES = { Function: "", GeneratorFunction: "*", AsyncFunction: "async ", AsyncGeneratorFunction: "async *", }; const TOKENS_PRECEDING_REGEXPS = new Set(("case delete else in instanceof new return throw typeof void " + ", ; : + - ! ~ & | ^ * / % < > ? =").split(" ")); /** * Track function parser usage. */ exports.USED_METHOD_KEY = new WeakSet(); /** * Stringify a function. */ const functionToString = (fn, space, next, key) => { const name = typeof key === "string" ? key : undefined; // Track in function parser for object stringify to avoid duplicate output. if (name !== undefined) exports.USED_METHOD_KEY.add(fn); return new FunctionParser(fn, space, next, name).stringify(); }; exports.functionToString = functionToString; /** * Rewrite a stringified function to remove initial indentation. */ function dedentFunction(fnString) { let found; for (const line of fnString.split("\n").slice(1)) { const m = /^[\s\t]+/.exec(line); if (!m) return fnString; // Early exit without indent. const [str] = m; if (found === undefined) found = str; else if (str.length < found.length) found = str; } return found ? fnString.split(`\n${found}`).join("\n") : fnString; } exports.dedentFunction = dedentFunction; /** * Function parser and stringify. */ class FunctionParser { constructor(fn, indent, next, key) { this.fn = fn; this.indent = indent; this.next = next; this.key = key; this.pos = 0; this.hadKeyword = false; this.fnString = Function.prototype.toString.call(fn); this.fnType = fn.constructor.name; this.keyQuote = key === undefined ? "" : quote_1.quoteKey(key, next); this.keyPrefix = key === undefined ? "" : `${this.keyQuote}:${indent ? " " : ""}`; this.isMethodCandidate = key === undefined ? false : this.fn.name === "" || this.fn.name === key; } stringify() { const value = this.tryParse(); // If we can't stringify this function, return a void expression; for // bonus help with debugging, include the function as a string literal. if (!value) { return `${this.keyPrefix}void ${this.next(this.fnString)}`; } return dedentFunction(value); } getPrefix() { if (this.isMethodCandidate && !this.hadKeyword) { return METHOD_PREFIXES[this.fnType] + this.keyQuote; } return this.keyPrefix + FUNCTION_PREFIXES[this.fnType]; } tryParse() { if (this.fnString[this.fnString.length - 1] !== "}") { // Must be an arrow function. return this.keyPrefix + this.fnString; } // Attempt to remove function prefix. if (this.fn.name) { const result = this.tryStrippingName(); if (result) return result; } // Support class expressions. const prevPos = this.pos; if (this.consumeSyntax() === "class") return this.fnString; this.pos = prevPos; if (this.tryParsePrefixTokens()) { const result = this.tryStrippingName(); if (result) return result; let offset = this.pos; switch (this.consumeSyntax("WORD_LIKE")) { case "WORD_LIKE": if (this.isMethodCandidate && !this.hadKeyword) { offset = this.pos; } case "()": if (this.fnString.substr(this.pos, 2) === "=>") { return this.keyPrefix + this.fnString; } this.pos = offset; case '"': case "'": case "[]": return this.getPrefix() + this.fnString.substr(this.pos); } } } /** * Attempt to parse the function from the current position by first stripping * the function's name from the front. This is not a fool-proof method on all * JavaScript engines, but yields good results on Node.js 4 (and slightly * less good results on Node.js 6 and 8). */ tryStrippingName() { if (METHOD_NAMES_ARE_QUOTED) { // ... then this approach is unnecessary and yields false positives. return; } let start = this.pos; const prefix = this.fnString.substr(this.pos, this.fn.name.length); if (prefix === this.fn.name) { this.pos += prefix.length; if (this.consumeSyntax() === "()" && this.consumeSyntax() === "{}" && this.pos === this.fnString.length) { // Don't include the function's name if it will be included in the // prefix, or if it's invalid as a name in a function expression. if (this.isMethodCandidate || !quote_1.isValidVariableName(prefix)) { start += prefix.length; } return this.getPrefix() + this.fnString.substr(start); } } this.pos = start; } /** * Attempt to advance the parser past the keywords expected to be at the * start of this function's definition. This method sets `this.hadKeyword` * based on whether or not a `function` keyword is consumed. */ tryParsePrefixTokens() { let posPrev = this.pos; this.hadKeyword = false; switch (this.fnType) { case "AsyncFunction": if (this.consumeSyntax() !== "async") return false; posPrev = this.pos; case "Function": if (this.consumeSyntax() === "function") { this.hadKeyword = true; } else { this.pos = posPrev; } return true; case "AsyncGeneratorFunction": if (this.consumeSyntax() !== "async") return false; case "GeneratorFunction": let token = this.consumeSyntax(); if (token === "function") { token = this.consumeSyntax(); this.hadKeyword = true; } return token === "*"; } } /** * Advance the parser past one element of JavaScript syntax. This could be a * matched pair of delimiters, like braces or parentheses, or an atomic unit * like a keyword, variable, or operator. Return a normalized string * representation of the element parsed--for example, returns '{}' for a * matched pair of braces. Comments and whitespace are skipped. * * (This isn't a full parser, so the token scanning logic used here is as * simple as it can be. As a consequence, some things that are one token in * JavaScript, like decimal number literals or most multi-character operators * like '&&', are split into more than one token here. However, awareness of * some multi-character sequences like '=>' is necessary, so we match the few * of them that we care about.) */ consumeSyntax(wordLikeToken) { const m = this.consumeMatch(/^(?:([A-Za-z_0-9$\xA0-\uFFFF]+)|=>|\+\+|\-\-|.)/); if (!m) return; const [token, match] = m; this.consumeWhitespace(); if (match) return wordLikeToken || match; switch (token) { case "(": return this.consumeSyntaxUntil("(", ")"); case "[": return this.consumeSyntaxUntil("[", "]"); case "{": return this.consumeSyntaxUntil("{", "}"); case "`": return this.consumeTemplate(); case '"': return this.consumeRegExp(/^(?:[^\\"]|\\.)*"/, '"'); case "'": return this.consumeRegExp(/^(?:[^\\']|\\.)*'/, "'"); } return token; } consumeSyntaxUntil(startToken, endToken) { let isRegExpAllowed = true; for (;;) { const token = this.consumeSyntax(); if (token === endToken) return startToken + endToken; if (!token || token === ")" || token === "]" || token === "}") return; if (token === "/" && isRegExpAllowed && this.consumeMatch(/^(?:\\.|[^\\\/\n[]|\[(?:\\.|[^\]])*\])+\/[a-z]*/)) { isRegExpAllowed = false; this.consumeWhitespace(); } else { isRegExpAllowed = TOKENS_PRECEDING_REGEXPS.has(token); } } } consumeMatch(re) { const m = re.exec(this.fnString.substr(this.pos)); if (m) this.pos += m[0].length; return m; } /** * Advance the parser past an arbitrary regular expression. Return `token`, * or the match object of the regexp. */ consumeRegExp(re, token) { const m = re.exec(this.fnString.substr(this.pos)); if (!m) return; this.pos += m[0].length; this.consumeWhitespace(); return token; } /** * Advance the parser past a template string. */ consumeTemplate() { for (;;) { this.consumeMatch(/^(?:[^`$\\]|\\.|\$(?!{))*/); if (this.fnString[this.pos] === "`") { this.pos++; this.consumeWhitespace(); return "`"; } if (this.fnString.substr(this.pos, 2) === "${") { this.pos += 2; this.consumeWhitespace(); if (this.consumeSyntaxUntil("{", "}")) continue; } return; } } /** * Advance the parser past any whitespace or comments. */ consumeWhitespace() { this.consumeMatch(/^(?:\s|\/\/.*|\/\*[^]*?\*\/)*/); } } exports.FunctionParser = FunctionParser; //# sourceMappingURL=function.js.map