303 lines
11 KiB
JavaScript
303 lines
11 KiB
JavaScript
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
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.
|
||
|
*/
|
||
|
exports.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();
|
||
|
};
|
||
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
// tslint:disable-next-line no-switch-case-fall-through
|
||
|
case "()":
|
||
|
if (this.fnString.substr(this.pos, 2) === "=>") {
|
||
|
return this.keyPrefix + this.fnString;
|
||
|
}
|
||
|
this.pos = offset;
|
||
|
// tslint:disable-next-line no-switch-case-fall-through
|
||
|
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.
|
||
|
*
|
||
|
* @return {boolean}
|
||
|
*/
|
||
|
tryParsePrefixTokens() {
|
||
|
let posPrev = this.pos;
|
||
|
this.hadKeyword = false;
|
||
|
switch (this.fnType) {
|
||
|
case "AsyncFunction":
|
||
|
if (this.consumeSyntax() !== "async")
|
||
|
return false;
|
||
|
posPrev = this.pos;
|
||
|
// tslint:disable-next-line no-switch-case-fall-through
|
||
|
case "Function":
|
||
|
if (this.consumeSyntax() === "function") {
|
||
|
this.hadKeyword = true;
|
||
|
}
|
||
|
else {
|
||
|
this.pos = posPrev;
|
||
|
}
|
||
|
return true;
|
||
|
case "AsyncGeneratorFunction":
|
||
|
if (this.consumeSyntax() !== "async")
|
||
|
return false;
|
||
|
// tslint:disable-next-line no-switch-case-fall-through
|
||
|
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 multicharacter operators
|
||
|
* like '&&', are split into more than one token here. However, awareness of
|
||
|
* some multicharacter 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
|