123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- "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
|