function.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const quote_1 = require("./quote");
  4. /**
  5. * Used in function stringification.
  6. */
  7. /* istanbul ignore next */
  8. const METHOD_NAMES_ARE_QUOTED = {
  9. " "() {
  10. /* Empty. */
  11. }
  12. }[" "]
  13. .toString()
  14. .charAt(0) === '"';
  15. const FUNCTION_PREFIXES = {
  16. Function: "function ",
  17. GeneratorFunction: "function* ",
  18. AsyncFunction: "async function ",
  19. AsyncGeneratorFunction: "async function* "
  20. };
  21. const METHOD_PREFIXES = {
  22. Function: "",
  23. GeneratorFunction: "*",
  24. AsyncFunction: "async ",
  25. AsyncGeneratorFunction: "async *"
  26. };
  27. const TOKENS_PRECEDING_REGEXPS = new Set(("case delete else in instanceof new return throw typeof void " +
  28. ", ; : + - ! ~ & | ^ * / % < > ? =").split(" "));
  29. /**
  30. * Track function parser usage.
  31. */
  32. exports.USED_METHOD_KEY = new WeakSet();
  33. /**
  34. * Stringify a function.
  35. */
  36. exports.functionToString = (fn, space, next, key) => {
  37. const name = typeof key === "string" ? key : undefined;
  38. // Track in function parser for object stringify to avoid duplicate output.
  39. if (name !== undefined)
  40. exports.USED_METHOD_KEY.add(fn);
  41. return new FunctionParser(fn, space, next, name).stringify();
  42. };
  43. /**
  44. * Rewrite a stringified function to remove initial indentation.
  45. */
  46. function dedentFunction(fnString) {
  47. let found;
  48. for (const line of fnString.split("\n").slice(1)) {
  49. const m = /^[\s\t]+/.exec(line);
  50. if (!m)
  51. return fnString; // Early exit without indent.
  52. const [str] = m;
  53. if (found === undefined)
  54. found = str;
  55. else if (str.length < found.length)
  56. found = str;
  57. }
  58. return found ? fnString.split(`\n${found}`).join("\n") : fnString;
  59. }
  60. exports.dedentFunction = dedentFunction;
  61. /**
  62. * Function parser and stringify.
  63. */
  64. class FunctionParser {
  65. constructor(fn, indent, next, key) {
  66. this.fn = fn;
  67. this.indent = indent;
  68. this.next = next;
  69. this.key = key;
  70. this.pos = 0;
  71. this.hadKeyword = false;
  72. this.fnString = Function.prototype.toString.call(fn);
  73. this.fnType = fn.constructor.name;
  74. this.keyQuote = key === undefined ? "" : quote_1.quoteKey(key, next);
  75. this.keyPrefix =
  76. key === undefined ? "" : `${this.keyQuote}:${indent ? " " : ""}`;
  77. this.isMethodCandidate =
  78. key === undefined ? false : this.fn.name === "" || this.fn.name === key;
  79. }
  80. stringify() {
  81. const value = this.tryParse();
  82. // If we can't stringify this function, return a void expression; for
  83. // bonus help with debugging, include the function as a string literal.
  84. if (!value) {
  85. return `${this.keyPrefix}void ${this.next(this.fnString)}`;
  86. }
  87. return dedentFunction(value);
  88. }
  89. getPrefix() {
  90. if (this.isMethodCandidate && !this.hadKeyword) {
  91. return METHOD_PREFIXES[this.fnType] + this.keyQuote;
  92. }
  93. return this.keyPrefix + FUNCTION_PREFIXES[this.fnType];
  94. }
  95. tryParse() {
  96. if (this.fnString[this.fnString.length - 1] !== "}") {
  97. // Must be an arrow function.
  98. return this.keyPrefix + this.fnString;
  99. }
  100. // Attempt to remove function prefix.
  101. if (this.fn.name) {
  102. const result = this.tryStrippingName();
  103. if (result)
  104. return result;
  105. }
  106. // Support class expressions.
  107. const prevPos = this.pos;
  108. if (this.consumeSyntax() === "class")
  109. return this.fnString;
  110. this.pos = prevPos;
  111. if (this.tryParsePrefixTokens()) {
  112. const result = this.tryStrippingName();
  113. if (result)
  114. return result;
  115. let offset = this.pos;
  116. switch (this.consumeSyntax("WORD_LIKE")) {
  117. case "WORD_LIKE":
  118. if (this.isMethodCandidate && !this.hadKeyword) {
  119. offset = this.pos;
  120. }
  121. // tslint:disable-next-line no-switch-case-fall-through
  122. case "()":
  123. if (this.fnString.substr(this.pos, 2) === "=>") {
  124. return this.keyPrefix + this.fnString;
  125. }
  126. this.pos = offset;
  127. // tslint:disable-next-line no-switch-case-fall-through
  128. case '"':
  129. case "'":
  130. case "[]":
  131. return this.getPrefix() + this.fnString.substr(this.pos);
  132. }
  133. }
  134. }
  135. /**
  136. * Attempt to parse the function from the current position by first stripping
  137. * the function's name from the front. This is not a fool-proof method on all
  138. * JavaScript engines, but yields good results on Node.js 4 (and slightly
  139. * less good results on Node.js 6 and 8).
  140. */
  141. tryStrippingName() {
  142. if (METHOD_NAMES_ARE_QUOTED) {
  143. // ... then this approach is unnecessary and yields false positives.
  144. return;
  145. }
  146. let start = this.pos;
  147. const prefix = this.fnString.substr(this.pos, this.fn.name.length);
  148. if (prefix === this.fn.name) {
  149. this.pos += prefix.length;
  150. if (this.consumeSyntax() === "()" &&
  151. this.consumeSyntax() === "{}" &&
  152. this.pos === this.fnString.length) {
  153. // Don't include the function's name if it will be included in the
  154. // prefix, or if it's invalid as a name in a function expression.
  155. if (this.isMethodCandidate || !quote_1.isValidVariableName(prefix)) {
  156. start += prefix.length;
  157. }
  158. return this.getPrefix() + this.fnString.substr(start);
  159. }
  160. }
  161. this.pos = start;
  162. }
  163. /**
  164. * Attempt to advance the parser past the keywords expected to be at the
  165. * start of this function's definition. This method sets `this.hadKeyword`
  166. * based on whether or not a `function` keyword is consumed.
  167. *
  168. * @return {boolean}
  169. */
  170. tryParsePrefixTokens() {
  171. let posPrev = this.pos;
  172. this.hadKeyword = false;
  173. switch (this.fnType) {
  174. case "AsyncFunction":
  175. if (this.consumeSyntax() !== "async")
  176. return false;
  177. posPrev = this.pos;
  178. // tslint:disable-next-line no-switch-case-fall-through
  179. case "Function":
  180. if (this.consumeSyntax() === "function") {
  181. this.hadKeyword = true;
  182. }
  183. else {
  184. this.pos = posPrev;
  185. }
  186. return true;
  187. case "AsyncGeneratorFunction":
  188. if (this.consumeSyntax() !== "async")
  189. return false;
  190. // tslint:disable-next-line no-switch-case-fall-through
  191. case "GeneratorFunction":
  192. let token = this.consumeSyntax();
  193. if (token === "function") {
  194. token = this.consumeSyntax();
  195. this.hadKeyword = true;
  196. }
  197. return token === "*";
  198. }
  199. }
  200. /**
  201. * Advance the parser past one element of JavaScript syntax. This could be a
  202. * matched pair of delimiters, like braces or parentheses, or an atomic unit
  203. * like a keyword, variable, or operator. Return a normalized string
  204. * representation of the element parsed--for example, returns '{}' for a
  205. * matched pair of braces. Comments and whitespace are skipped.
  206. *
  207. * (This isn't a full parser, so the token scanning logic used here is as
  208. * simple as it can be. As a consequence, some things that are one token in
  209. * JavaScript, like decimal number literals or most multicharacter operators
  210. * like '&&', are split into more than one token here. However, awareness of
  211. * some multicharacter sequences like '=>' is necessary, so we match the few
  212. * of them that we care about.)
  213. */
  214. consumeSyntax(wordLikeToken) {
  215. const m = this.consumeMatch(/^(?:([A-Za-z_0-9$\xA0-\uFFFF]+)|=>|\+\+|\-\-|.)/);
  216. if (!m)
  217. return;
  218. const [token, match] = m;
  219. this.consumeWhitespace();
  220. if (match)
  221. return wordLikeToken || match;
  222. switch (token) {
  223. case "(":
  224. return this.consumeSyntaxUntil("(", ")");
  225. case "[":
  226. return this.consumeSyntaxUntil("[", "]");
  227. case "{":
  228. return this.consumeSyntaxUntil("{", "}");
  229. case "`":
  230. return this.consumeTemplate();
  231. case '"':
  232. return this.consumeRegExp(/^(?:[^\\"]|\\.)*"/, '"');
  233. case "'":
  234. return this.consumeRegExp(/^(?:[^\\']|\\.)*'/, "'");
  235. }
  236. return token;
  237. }
  238. consumeSyntaxUntil(startToken, endToken) {
  239. let isRegExpAllowed = true;
  240. for (;;) {
  241. const token = this.consumeSyntax();
  242. if (token === endToken)
  243. return startToken + endToken;
  244. if (!token || token === ")" || token === "]" || token === "}")
  245. return;
  246. if (token === "/" &&
  247. isRegExpAllowed &&
  248. this.consumeMatch(/^(?:\\.|[^\\\/\n[]|\[(?:\\.|[^\]])*\])+\/[a-z]*/)) {
  249. isRegExpAllowed = false;
  250. this.consumeWhitespace();
  251. }
  252. else {
  253. isRegExpAllowed = TOKENS_PRECEDING_REGEXPS.has(token);
  254. }
  255. }
  256. }
  257. consumeMatch(re) {
  258. const m = re.exec(this.fnString.substr(this.pos));
  259. if (m)
  260. this.pos += m[0].length;
  261. return m;
  262. }
  263. /**
  264. * Advance the parser past an arbitrary regular expression. Return `token`,
  265. * or the match object of the regexp.
  266. */
  267. consumeRegExp(re, token) {
  268. const m = re.exec(this.fnString.substr(this.pos));
  269. if (!m)
  270. return;
  271. this.pos += m[0].length;
  272. this.consumeWhitespace();
  273. return token;
  274. }
  275. /**
  276. * Advance the parser past a template string.
  277. */
  278. consumeTemplate() {
  279. for (;;) {
  280. this.consumeMatch(/^(?:[^`$\\]|\\.|\$(?!{))*/);
  281. if (this.fnString[this.pos] === "`") {
  282. this.pos++;
  283. this.consumeWhitespace();
  284. return "`";
  285. }
  286. if (this.fnString.substr(this.pos, 2) === "${") {
  287. this.pos += 2;
  288. this.consumeWhitespace();
  289. if (this.consumeSyntaxUntil("{", "}"))
  290. continue;
  291. }
  292. return;
  293. }
  294. }
  295. /**
  296. * Advance the parser past any whitespace or comments.
  297. */
  298. consumeWhitespace() {
  299. this.consumeMatch(/^(?:\s|\/\/.*|\/\*[^]*?\*\/)*/);
  300. }
  301. }
  302. exports.FunctionParser = FunctionParser;
  303. //# sourceMappingURL=function.js.map