299 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "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
 |