You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
11 KiB
299 lines
11 KiB
"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
|