es6-class-visitors.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /**
  2. * Copyright 2013-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. /*jslint node:true*/
  10. /**
  11. * @typechecks
  12. */
  13. 'use strict';
  14. var base62 = require('base62');
  15. var Syntax = require('esprima-fb').Syntax;
  16. var utils = require('../src/utils');
  17. var reservedWordsHelper = require('./reserved-words-helper');
  18. var declareIdentInLocalScope = utils.declareIdentInLocalScope;
  19. var initScopeMetadata = utils.initScopeMetadata;
  20. var SUPER_PROTO_IDENT_PREFIX = '____SuperProtoOf';
  21. var _anonClassUUIDCounter = 0;
  22. var _mungedSymbolMaps = {};
  23. function resetSymbols() {
  24. _anonClassUUIDCounter = 0;
  25. _mungedSymbolMaps = {};
  26. }
  27. /**
  28. * Used to generate a unique class for use with code-gens for anonymous class
  29. * expressions.
  30. *
  31. * @param {object} state
  32. * @return {string}
  33. */
  34. function _generateAnonymousClassName(state) {
  35. var mungeNamespace = state.mungeNamespace || '';
  36. return '____Class' + mungeNamespace + base62.encode(_anonClassUUIDCounter++);
  37. }
  38. /**
  39. * Given an identifier name, munge it using the current state's mungeNamespace.
  40. *
  41. * @param {string} identName
  42. * @param {object} state
  43. * @return {string}
  44. */
  45. function _getMungedName(identName, state) {
  46. var mungeNamespace = state.mungeNamespace;
  47. var shouldMinify = state.g.opts.minify;
  48. if (shouldMinify) {
  49. if (!_mungedSymbolMaps[mungeNamespace]) {
  50. _mungedSymbolMaps[mungeNamespace] = {
  51. symbolMap: {},
  52. identUUIDCounter: 0
  53. };
  54. }
  55. var symbolMap = _mungedSymbolMaps[mungeNamespace].symbolMap;
  56. if (!symbolMap[identName]) {
  57. symbolMap[identName] =
  58. base62.encode(_mungedSymbolMaps[mungeNamespace].identUUIDCounter++);
  59. }
  60. identName = symbolMap[identName];
  61. }
  62. return '$' + mungeNamespace + identName;
  63. }
  64. /**
  65. * Extracts super class information from a class node.
  66. *
  67. * Information includes name of the super class and/or the expression string
  68. * (if extending from an expression)
  69. *
  70. * @param {object} node
  71. * @param {object} state
  72. * @return {object}
  73. */
  74. function _getSuperClassInfo(node, state) {
  75. var ret = {
  76. name: null,
  77. expression: null
  78. };
  79. if (node.superClass) {
  80. if (node.superClass.type === Syntax.Identifier) {
  81. ret.name = node.superClass.name;
  82. } else {
  83. // Extension from an expression
  84. ret.name = _generateAnonymousClassName(state);
  85. ret.expression = state.g.source.substring(
  86. node.superClass.range[0],
  87. node.superClass.range[1]
  88. );
  89. }
  90. }
  91. return ret;
  92. }
  93. /**
  94. * Used with .filter() to find the constructor method in a list of
  95. * MethodDefinition nodes.
  96. *
  97. * @param {object} classElement
  98. * @return {boolean}
  99. */
  100. function _isConstructorMethod(classElement) {
  101. return classElement.type === Syntax.MethodDefinition &&
  102. classElement.key.type === Syntax.Identifier &&
  103. classElement.key.name === 'constructor';
  104. }
  105. /**
  106. * @param {object} node
  107. * @param {object} state
  108. * @return {boolean}
  109. */
  110. function _shouldMungeIdentifier(node, state) {
  111. return (
  112. !!state.methodFuncNode &&
  113. !utils.getDocblock(state).hasOwnProperty('preventMunge') &&
  114. /^_(?!_)/.test(node.name)
  115. );
  116. }
  117. /**
  118. * @param {function} traverse
  119. * @param {object} node
  120. * @param {array} path
  121. * @param {object} state
  122. */
  123. function visitClassMethod(traverse, node, path, state) {
  124. if (!state.g.opts.es5 && (node.kind === 'get' || node.kind === 'set')) {
  125. throw new Error(
  126. 'This transform does not support ' + node.kind + 'ter methods for ES6 ' +
  127. 'classes. (line: ' + node.loc.start.line + ', col: ' +
  128. node.loc.start.column + ')'
  129. );
  130. }
  131. state = utils.updateState(state, {
  132. methodNode: node
  133. });
  134. utils.catchup(node.range[0], state);
  135. path.unshift(node);
  136. traverse(node.value, path, state);
  137. path.shift();
  138. return false;
  139. }
  140. visitClassMethod.test = function(node, path, state) {
  141. return node.type === Syntax.MethodDefinition;
  142. };
  143. /**
  144. * @param {function} traverse
  145. * @param {object} node
  146. * @param {array} path
  147. * @param {object} state
  148. */
  149. function visitClassFunctionExpression(traverse, node, path, state) {
  150. var methodNode = path[0];
  151. var isGetter = methodNode.kind === 'get';
  152. var isSetter = methodNode.kind === 'set';
  153. state = utils.updateState(state, {
  154. methodFuncNode: node
  155. });
  156. if (methodNode.key.name === 'constructor') {
  157. utils.append('function ' + state.className, state);
  158. } else {
  159. var methodAccessorComputed = methodNode.computed;
  160. var methodAccessor;
  161. var prototypeOrStatic = methodNode.static ? '' : '.prototype';
  162. var objectAccessor = state.className + prototypeOrStatic;
  163. if (methodNode.key.type === Syntax.Identifier) {
  164. // foo() {}
  165. methodAccessor = methodNode.key.name;
  166. if (_shouldMungeIdentifier(methodNode.key, state)) {
  167. methodAccessor = _getMungedName(methodAccessor, state);
  168. }
  169. if (isGetter || isSetter) {
  170. methodAccessor = JSON.stringify(methodAccessor);
  171. } else if (reservedWordsHelper.isReservedWord(methodAccessor)) {
  172. methodAccessorComputed = true;
  173. methodAccessor = JSON.stringify(methodAccessor);
  174. }
  175. } else if (methodNode.key.type === Syntax.Literal) {
  176. // 'foo bar'() {} | get 'foo bar'() {} | set 'foo bar'() {}
  177. methodAccessor = JSON.stringify(methodNode.key.value);
  178. methodAccessorComputed = true;
  179. }
  180. if (isSetter || isGetter) {
  181. utils.append(
  182. 'Object.defineProperty(' +
  183. objectAccessor + ',' +
  184. methodAccessor + ',' +
  185. '{configurable:true,' +
  186. methodNode.kind + ':function',
  187. state
  188. );
  189. } else {
  190. if (state.g.opts.es3) {
  191. if (methodAccessorComputed) {
  192. methodAccessor = '[' + methodAccessor + ']';
  193. } else {
  194. methodAccessor = '.' + methodAccessor;
  195. }
  196. utils.append(
  197. objectAccessor +
  198. methodAccessor + '=function' + (node.generator ? '*' : ''),
  199. state
  200. );
  201. } else {
  202. if (!methodAccessorComputed) {
  203. methodAccessor = JSON.stringify(methodAccessor);
  204. }
  205. utils.append(
  206. 'Object.defineProperty(' +
  207. objectAccessor + ',' +
  208. methodAccessor + ',' +
  209. '{writable:true,configurable:true,' +
  210. 'value:function' + (node.generator ? '*' : ''),
  211. state
  212. );
  213. }
  214. }
  215. }
  216. utils.move(methodNode.key.range[1], state);
  217. utils.append('(', state);
  218. var params = node.params;
  219. if (params.length > 0) {
  220. utils.catchupNewlines(params[0].range[0], state);
  221. for (var i = 0; i < params.length; i++) {
  222. utils.catchup(node.params[i].range[0], state);
  223. path.unshift(node);
  224. traverse(params[i], path, state);
  225. path.shift();
  226. }
  227. }
  228. var closingParenPosition = utils.getNextSyntacticCharOffset(')', state);
  229. utils.catchupWhiteSpace(closingParenPosition, state);
  230. var openingBracketPosition = utils.getNextSyntacticCharOffset('{', state);
  231. utils.catchup(openingBracketPosition + 1, state);
  232. if (!state.scopeIsStrict) {
  233. utils.append('"use strict";', state);
  234. state = utils.updateState(state, {
  235. scopeIsStrict: true
  236. });
  237. }
  238. utils.move(node.body.range[0] + '{'.length, state);
  239. path.unshift(node);
  240. traverse(node.body, path, state);
  241. path.shift();
  242. utils.catchup(node.body.range[1], state);
  243. if (methodNode.key.name !== 'constructor') {
  244. if (isGetter || isSetter || !state.g.opts.es3) {
  245. utils.append('})', state);
  246. }
  247. utils.append(';', state);
  248. }
  249. return false;
  250. }
  251. visitClassFunctionExpression.test = function(node, path, state) {
  252. return node.type === Syntax.FunctionExpression
  253. && path[0].type === Syntax.MethodDefinition;
  254. };
  255. function visitClassMethodParam(traverse, node, path, state) {
  256. var paramName = node.name;
  257. if (_shouldMungeIdentifier(node, state)) {
  258. paramName = _getMungedName(node.name, state);
  259. }
  260. utils.append(paramName, state);
  261. utils.move(node.range[1], state);
  262. }
  263. visitClassMethodParam.test = function(node, path, state) {
  264. if (!path[0] || !path[1]) {
  265. return;
  266. }
  267. var parentFuncExpr = path[0];
  268. var parentClassMethod = path[1];
  269. return parentFuncExpr.type === Syntax.FunctionExpression
  270. && parentClassMethod.type === Syntax.MethodDefinition
  271. && node.type === Syntax.Identifier;
  272. };
  273. /**
  274. * @param {function} traverse
  275. * @param {object} node
  276. * @param {array} path
  277. * @param {object} state
  278. */
  279. function _renderClassBody(traverse, node, path, state) {
  280. var className = state.className;
  281. var superClass = state.superClass;
  282. // Set up prototype of constructor on same line as `extends` for line-number
  283. // preservation. This relies on function-hoisting if a constructor function is
  284. // defined in the class body.
  285. if (superClass.name) {
  286. // If the super class is an expression, we need to memoize the output of the
  287. // expression into the generated class name variable and use that to refer
  288. // to the super class going forward. Example:
  289. //
  290. // class Foo extends mixin(Bar, Baz) {}
  291. // --transforms to--
  292. // function Foo() {} var ____Class0Blah = mixin(Bar, Baz);
  293. if (superClass.expression !== null) {
  294. utils.append(
  295. 'var ' + superClass.name + '=' + superClass.expression + ';',
  296. state
  297. );
  298. }
  299. var keyName = superClass.name + '____Key';
  300. var keyNameDeclarator = '';
  301. if (!utils.identWithinLexicalScope(keyName, state)) {
  302. keyNameDeclarator = 'var ';
  303. declareIdentInLocalScope(keyName, initScopeMetadata(node), state);
  304. }
  305. utils.append(
  306. 'for(' + keyNameDeclarator + keyName + ' in ' + superClass.name + '){' +
  307. 'if(' + superClass.name + '.hasOwnProperty(' + keyName + ')){' +
  308. className + '[' + keyName + ']=' +
  309. superClass.name + '[' + keyName + '];' +
  310. '}' +
  311. '}',
  312. state
  313. );
  314. var superProtoIdentStr = SUPER_PROTO_IDENT_PREFIX + superClass.name;
  315. if (!utils.identWithinLexicalScope(superProtoIdentStr, state)) {
  316. utils.append(
  317. 'var ' + superProtoIdentStr + '=' + superClass.name + '===null?' +
  318. 'null:' + superClass.name + '.prototype;',
  319. state
  320. );
  321. declareIdentInLocalScope(superProtoIdentStr, initScopeMetadata(node), state);
  322. }
  323. utils.append(
  324. className + '.prototype=Object.create(' + superProtoIdentStr + ');',
  325. state
  326. );
  327. utils.append(
  328. className + '.prototype.constructor=' + className + ';',
  329. state
  330. );
  331. utils.append(
  332. className + '.__superConstructor__=' + superClass.name + ';',
  333. state
  334. );
  335. }
  336. // If there's no constructor method specified in the class body, create an
  337. // empty constructor function at the top (same line as the class keyword)
  338. if (!node.body.body.filter(_isConstructorMethod).pop()) {
  339. utils.append('function ' + className + '(){', state);
  340. if (!state.scopeIsStrict) {
  341. utils.append('"use strict";', state);
  342. }
  343. if (superClass.name) {
  344. utils.append(
  345. 'if(' + superClass.name + '!==null){' +
  346. superClass.name + '.apply(this,arguments);}',
  347. state
  348. );
  349. }
  350. utils.append('}', state);
  351. }
  352. utils.move(node.body.range[0] + '{'.length, state);
  353. traverse(node.body, path, state);
  354. utils.catchupWhiteSpace(node.range[1], state);
  355. }
  356. /**
  357. * @param {function} traverse
  358. * @param {object} node
  359. * @param {array} path
  360. * @param {object} state
  361. */
  362. function visitClassDeclaration(traverse, node, path, state) {
  363. var className = node.id.name;
  364. var superClass = _getSuperClassInfo(node, state);
  365. state = utils.updateState(state, {
  366. mungeNamespace: className,
  367. className: className,
  368. superClass: superClass
  369. });
  370. _renderClassBody(traverse, node, path, state);
  371. return false;
  372. }
  373. visitClassDeclaration.test = function(node, path, state) {
  374. return node.type === Syntax.ClassDeclaration;
  375. };
  376. /**
  377. * @param {function} traverse
  378. * @param {object} node
  379. * @param {array} path
  380. * @param {object} state
  381. */
  382. function visitClassExpression(traverse, node, path, state) {
  383. var className = node.id && node.id.name || _generateAnonymousClassName(state);
  384. var superClass = _getSuperClassInfo(node, state);
  385. utils.append('(function(){', state);
  386. state = utils.updateState(state, {
  387. mungeNamespace: className,
  388. className: className,
  389. superClass: superClass
  390. });
  391. _renderClassBody(traverse, node, path, state);
  392. utils.append('return ' + className + ';})()', state);
  393. return false;
  394. }
  395. visitClassExpression.test = function(node, path, state) {
  396. return node.type === Syntax.ClassExpression;
  397. };
  398. /**
  399. * @param {function} traverse
  400. * @param {object} node
  401. * @param {array} path
  402. * @param {object} state
  403. */
  404. function visitPrivateIdentifier(traverse, node, path, state) {
  405. utils.append(_getMungedName(node.name, state), state);
  406. utils.move(node.range[1], state);
  407. }
  408. visitPrivateIdentifier.test = function(node, path, state) {
  409. if (node.type === Syntax.Identifier && _shouldMungeIdentifier(node, state)) {
  410. // Always munge non-computed properties of MemberExpressions
  411. // (a la preventing access of properties of unowned objects)
  412. if (path[0].type === Syntax.MemberExpression && path[0].object !== node
  413. && path[0].computed === false) {
  414. return true;
  415. }
  416. // Always munge identifiers that were declared within the method function
  417. // scope
  418. if (utils.identWithinLexicalScope(node.name, state, state.methodFuncNode)) {
  419. return true;
  420. }
  421. // Always munge private keys on object literals defined within a method's
  422. // scope.
  423. if (path[0].type === Syntax.Property
  424. && path[1].type === Syntax.ObjectExpression) {
  425. return true;
  426. }
  427. // Always munge function parameters
  428. if (path[0].type === Syntax.FunctionExpression
  429. || path[0].type === Syntax.FunctionDeclaration
  430. || path[0].type === Syntax.ArrowFunctionExpression) {
  431. for (var i = 0; i < path[0].params.length; i++) {
  432. if (path[0].params[i] === node) {
  433. return true;
  434. }
  435. }
  436. }
  437. }
  438. return false;
  439. };
  440. /**
  441. * @param {function} traverse
  442. * @param {object} node
  443. * @param {array} path
  444. * @param {object} state
  445. */
  446. function visitSuperCallExpression(traverse, node, path, state) {
  447. var superClassName = state.superClass.name;
  448. if (node.callee.type === Syntax.Identifier) {
  449. if (_isConstructorMethod(state.methodNode)) {
  450. utils.append(superClassName + '.call(', state);
  451. } else {
  452. var protoProp = SUPER_PROTO_IDENT_PREFIX + superClassName;
  453. if (state.methodNode.key.type === Syntax.Identifier) {
  454. protoProp += '.' + state.methodNode.key.name;
  455. } else if (state.methodNode.key.type === Syntax.Literal) {
  456. protoProp += '[' + JSON.stringify(state.methodNode.key.value) + ']';
  457. }
  458. utils.append(protoProp + ".call(", state);
  459. }
  460. utils.move(node.callee.range[1], state);
  461. } else if (node.callee.type === Syntax.MemberExpression) {
  462. utils.append(SUPER_PROTO_IDENT_PREFIX + superClassName, state);
  463. utils.move(node.callee.object.range[1], state);
  464. if (node.callee.computed) {
  465. // ["a" + "b"]
  466. utils.catchup(node.callee.property.range[1] + ']'.length, state);
  467. } else {
  468. // .ab
  469. utils.append('.' + node.callee.property.name, state);
  470. }
  471. utils.append('.call(', state);
  472. utils.move(node.callee.range[1], state);
  473. }
  474. utils.append('this', state);
  475. if (node.arguments.length > 0) {
  476. utils.append(',', state);
  477. utils.catchupWhiteSpace(node.arguments[0].range[0], state);
  478. traverse(node.arguments, path, state);
  479. }
  480. utils.catchupWhiteSpace(node.range[1], state);
  481. utils.append(')', state);
  482. return false;
  483. }
  484. visitSuperCallExpression.test = function(node, path, state) {
  485. if (state.superClass && node.type === Syntax.CallExpression) {
  486. var callee = node.callee;
  487. if (callee.type === Syntax.Identifier && callee.name === 'super'
  488. || callee.type == Syntax.MemberExpression
  489. && callee.object.name === 'super') {
  490. return true;
  491. }
  492. }
  493. return false;
  494. };
  495. /**
  496. * @param {function} traverse
  497. * @param {object} node
  498. * @param {array} path
  499. * @param {object} state
  500. */
  501. function visitSuperMemberExpression(traverse, node, path, state) {
  502. var superClassName = state.superClass.name;
  503. utils.append(SUPER_PROTO_IDENT_PREFIX + superClassName, state);
  504. utils.move(node.object.range[1], state);
  505. }
  506. visitSuperMemberExpression.test = function(node, path, state) {
  507. return state.superClass
  508. && node.type === Syntax.MemberExpression
  509. && node.object.type === Syntax.Identifier
  510. && node.object.name === 'super';
  511. };
  512. exports.resetSymbols = resetSymbols;
  513. exports.visitorList = [
  514. visitClassDeclaration,
  515. visitClassExpression,
  516. visitClassFunctionExpression,
  517. visitClassMethod,
  518. visitClassMethodParam,
  519. visitPrivateIdentifier,
  520. visitSuperCallExpression,
  521. visitSuperMemberExpression
  522. ];