The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── bin
    └── index.js
├── index.js
├── package-lock.json
├── package.json
├── src
    ├── Cli.js
    ├── Logger.js
    ├── OptionParser.js
    ├── Parser.js
    ├── Transformer.js
    ├── createTransformer.js
    ├── io.js
    ├── scope
    │   ├── BlockScope.js
    │   ├── FunctionHoister.js
    │   ├── FunctionScope.js
    │   ├── Scope.js
    │   ├── ScopeManager.js
    │   ├── Variable.js
    │   ├── VariableGroup.js
    │   └── VariableMarker.js
    ├── syntax
    │   ├── ArrowFunctionExpression.js
    │   ├── BaseSyntax.js
    │   ├── ExportNamedDeclaration.js
    │   ├── ImportDeclaration.js
    │   ├── ImportDefaultSpecifier.js
    │   ├── ImportSpecifier.js
    │   ├── TemplateElement.js
    │   ├── TemplateLiteral.js
    │   └── VariableDeclaration.js
    ├── transform
    │   ├── argRest.js
    │   ├── argSpread.js
    │   ├── arrow.js
    │   ├── arrowReturn.js
    │   ├── class
    │   │   ├── PotentialClass.js
    │   │   ├── PotentialConstructor.js
    │   │   ├── PotentialMethod.js
    │   │   ├── extractComments.js
    │   │   ├── index.js
    │   │   ├── inheritance
    │   │   │   ├── ImportUtilDetector.js
    │   │   │   ├── Inheritance.js
    │   │   │   ├── Prototypal.js
    │   │   │   ├── RequireUtilDetector.js
    │   │   │   ├── RequireUtilInheritsDetector.js
    │   │   │   └── UtilInherits.js
    │   │   ├── isFunctionProperty.js
    │   │   ├── isTransformableToMethod.js
    │   │   ├── matchFunctionAssignment.js
    │   │   ├── matchFunctionDeclaration.js
    │   │   ├── matchFunctionVar.js
    │   │   ├── matchObjectDefinePropertiesCall.js
    │   │   ├── matchObjectDefinePropertyCall.js
    │   │   ├── matchPrototypeFunctionAssignment.js
    │   │   └── matchPrototypeObjectAssignment.js
    │   ├── commonjs
    │   │   ├── exportCommonjs.js
    │   │   ├── importCommonjs.js
    │   │   ├── index.js
    │   │   ├── isExports.js
    │   │   ├── isModuleExports.js
    │   │   ├── isVarWithRequireCalls.js
    │   │   ├── matchDefaultExport.js
    │   │   ├── matchNamedExport.js
    │   │   └── matchRequire.js
    │   ├── defaultParam
    │   │   ├── index.js
    │   │   ├── matchIfUndefinedAssignment.js
    │   │   ├── matchOrAssignment.js
    │   │   └── matchTernaryAssignment.js
    │   ├── destructParam.js
    │   ├── exponent.js
    │   ├── forEach
    │   │   ├── index.js
    │   │   └── validateForLoop.js
    │   ├── forOf.js
    │   ├── includes
    │   │   ├── comparison.js
    │   │   ├── index.js
    │   │   └── matchesIndexOf.js
    │   ├── let.js
    │   ├── multiVar.js
    │   ├── noStrict.js
    │   ├── objMethod.js
    │   ├── objShorthand.js
    │   └── template.js
    ├── traverser.js
    ├── utils
    │   ├── Hierarchy.js
    │   ├── copyComments.js
    │   ├── destructuring.js
    │   ├── functionType.js
    │   ├── isEqualAst.js
    │   ├── isString.js
    │   ├── matchAliasedForLoop.js
    │   ├── multiReplaceStatement.js
    │   └── variableType.js
    └── withScope.js
├── system-test
    ├── binTest.js
    ├── commonjsApiTest.js
    ├── importApiTest.js
    └── testTransformApi.js
├── test
    ├── OptionParserTest.js
    ├── createTestHelpers.js
    └── transform
    │   ├── argRestTest.js
    │   ├── argSpreadTest.js
    │   ├── arrowReturnTest.js
    │   ├── arrowTest.js
    │   ├── classInheritanceTest.js
    │   ├── classTest.js
    │   ├── commonjs
    │       ├── exportCommonjsTest.js
    │       └── importCommonjsTest.js
    │   ├── defaultParamTest.js
    │   ├── destructParamTest.js
    │   ├── ecmaVersionTest.js
    │   ├── exponentTest.js
    │   ├── forEachTest.js
    │   ├── forOfTest.js
    │   ├── includesTest.js
    │   ├── jsxTest.js
    │   ├── letTest.js
    │   ├── multiVarTest.js
    │   ├── noStrictTest.js
    │   ├── objMethodTest.js
    │   ├── objShorthandTest.js
    │   ├── restSpreadTest.js
    │   ├── templateTest.js
    │   └── whitespaceTest.js
└── types
    └── index.d.ts


/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 |   "presets": [
3 |     "@babel/preset-env"
4 |   ]
5 | }


--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
 1 | [*]
 2 | indent_style = space
 3 | end_of_line = lf
 4 | indent_size = 2
 5 | charset = utf-8
 6 | trim_trailing_whitespace = true
 7 | 
 8 | [*.md]
 9 | max_line_length = 0
10 | trim_trailing_whitespace = false
11 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.log
3 | node_modules
4 | lib
5 | .nyc_output
6 | coverage
7 | *.lcov
8 | yarn.lock
9 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | language: node_js
 2 | node_js:
 3 |   - "6"
 4 |   - "8"
 5 |   - "10"
 6 |   - "12"
 7 | cache:
 8 |   directories:
 9 |   - node_modules
10 | 
11 | before_script:
12 | - npm run lint
13 | 
14 | script:
15 | - npm run system-test
16 | - npm run coverage
17 | - npm run ensure-coverage
18 | 
19 | after_script:
20 | - npm run upload-coverage
21 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) 2015 Mohamad Mohebifar
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
23 | 


--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const Cli = require('./../lib/Cli').default;
3 | 
4 | new Cli(process.argv).run();
5 | 


--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
 1 | const createTransformer = require('./lib/createTransformer').default;
 2 | 
 3 | /**
 4 |  * Exposes API similar to Babel:
 5 |  *
 6 |  *     import lebab from "lebab";
 7 |  *     const {code, warnings} = lebab.transform('Some JS', ['let', 'arrow']);
 8 |  *
 9 |  * @param  {String} code The code to transform
10 |  * @param  {String[]} transformNames The transforms to apply
11 |  * @return {Object} An object with code and warnings props
12 |  */
13 | exports.transform = function(code, transformNames) {
14 |   return createTransformer(transformNames).run(code);
15 | };
16 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "lebab",
 3 |   "version": "3.2.7",
 4 |   "description": "Turn your ES5 code into readable ES6/ES7",
 5 |   "main": "index.js",
 6 |   "scripts": {
 7 |     "lint": "eslint src/ test/ system-test/ bin/ *.js",
 8 |     "build": "rm -rf lib/ && babel src/ --out-dir lib/",
 9 |     "prepublishOnly": "npm run build",
10 |     "system-test": "npm run build && mocha --require @babel/register \"./system-test/**/*Test.js\"",
11 |     "//": "Unit tests: a) for single run, b) in watch-mode, c) with coverage.",
12 |     "test": "mocha --require @babel/register \"./test/**/*Test.js\"",
13 |     "watch": "mocha --watch --require @babel/register \"./test/**/*Test.js\"",
14 |     "coverage": "nyc npm test",
15 |     "///": "These are used by Travis to create coverage report. Run 'coverage' script first.",
16 |     "ensure-coverage": "nyc check-coverage --statements 80 --branches 80 --functions 80",
17 |     "upload-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov"
18 |   },
19 |   "types": "./types/index.d.ts",
20 |   "engines": {
21 |     "node": ">=6"
22 |   },
23 |   "files": [
24 |     "lib",
25 |     "bin",
26 |     "index.js"
27 |   ],
28 |   "repository": {
29 |     "type": "git",
30 |     "url": "https://github.com/lebab/lebab"
31 |   },
32 |   "bin": {
33 |     "lebab": "bin/index.js"
34 |   },
35 |   "keywords": [
36 |     "es5",
37 |     "es6",
38 |     "es2015",
39 |     "es7",
40 |     "es2016",
41 |     "transpiler",
42 |     "transpile"
43 |   ],
44 |   "author": "Mohamad Mohebifar <mohamad@mohebifar.com>",
45 |   "license": "MIT",
46 |   "bugs": {
47 |     "url": "https://github.com/lebab/lebab/issues"
48 |   },
49 |   "homepage": "https://github.com/lebab/lebab",
50 |   "dependencies": {
51 |     "commander": "^11.0.0",
52 |     "escope": "^4.0.0",
53 |     "espree": "^9.6.1",
54 |     "estraverse": "^5.3.0",
55 |     "f-matches": "^1.1.0",
56 |     "glob": "^10.3.3",
57 |     "lodash": "^4.17.21",
58 |     "recast": "^0.23.4"
59 |   },
60 |   "devDependencies": {
61 |     "@babel/cli": "^7.22.10",
62 |     "@babel/core": "^7.22.10",
63 |     "@babel/preset-env": "^7.22.10",
64 |     "@babel/register": "^7.22.5",
65 |     "chai": "^4.3.7",
66 |     "codecov": "^3.8.2",
67 |     "eslint": "^8.47.0",
68 |     "eslint-plugin-no-null": "^1.0.2",
69 |     "mocha": "^10.2.0",
70 |     "nyc": "^15.1.0"
71 |   }
72 | }
73 | 


--------------------------------------------------------------------------------
/src/Cli.js:
--------------------------------------------------------------------------------
 1 | import {glob} from 'glob';
 2 | import OptionParser from './OptionParser';
 3 | import createTransformer from './createTransformer';
 4 | import io from './io';
 5 | 
 6 | /**
 7 |  * Lebab command line app
 8 |  */
 9 | export default class Cli {
10 |   /**
11 |    * @param {String[]} argv Command line arguments
12 |    */
13 |   constructor(argv) {
14 |     try {
15 |       this.options = new OptionParser().parse(argv);
16 |       this.transformer = createTransformer(this.options.transforms);
17 |     }
18 |     catch (error) {
19 |       console.error(error); // eslint-disable-line no-console
20 |       process.exit(2);
21 |     }
22 |   }
23 | 
24 |   /**
25 |    * Runs the app
26 |    */
27 |   run() {
28 |     if (this.options.replace) {
29 |       // Transform all files in a directory
30 |       glob.sync(this.options.replace).forEach((file) => {
31 |         this.transformFile(file, file);
32 |       });
33 |     }
34 |     else {
35 |       // Transform just a single file
36 |       this.transformFile(this.options.inFile, this.options.outFile);
37 |     }
38 |   }
39 | 
40 |   transformFile(inFile, outFile) {
41 |     try {
42 |       const {code, warnings} = this.transformer.run(io.read(inFile));
43 | 
44 |       // Log warnings if there are any
45 |       if (warnings.length > 0 && inFile) {
46 |         console.error(`${inFile}:`); // eslint-disable-line no-console
47 |       }
48 | 
49 |       warnings.forEach(({line, msg, type}) => {
50 |         console.error( // eslint-disable-line no-console
51 |           `${line}:  warning  ${msg}  (${type})`
52 |         );
53 |       });
54 | 
55 |       io.write(outFile, code);
56 |     }
57 |     catch (e) {
58 |       console.error(`Error transforming: ${inFile}\n`); // eslint-disable-line no-console
59 |       throw e;
60 |     }
61 |   }
62 | }
63 | 


--------------------------------------------------------------------------------
/src/Logger.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Passed to transforms so they can log warnings.
 3 |  */
 4 | export default class Logger {
 5 |   constructor() {
 6 |     this.warnings = [];
 7 |   }
 8 | 
 9 |   /**
10 |    * Logs a warning.
11 |    * @param  {Object} node AAST node that caused the warning
12 |    * @param  {String} msg Warning message itself
13 |    * @param  {String} type Name of the transform
14 |    */
15 |   warn(node, msg, type) {
16 |     this.warnings.push({
17 |       line: node.loc ? node.loc.start.line : 0,
18 |       msg,
19 |       type,
20 |     });
21 |   }
22 | 
23 |   /**
24 |    * Returns list of all the warnings
25 |    * @return {Object[]}
26 |    */
27 |   getWarnings() {
28 |     return this.warnings;
29 |   }
30 | }
31 | 


--------------------------------------------------------------------------------
/src/OptionParser.js:
--------------------------------------------------------------------------------
  1 | import {Command} from 'commander';
  2 | import pkg from '../package.json';
  3 | import fs from 'fs';
  4 | import path from 'path';
  5 | 
  6 | const transformsDocs = `
  7 |   Safe transforms:
  8 | 
  9 |     + arrow .......... callback to arrow function
 10 |     + arrow-return ... drop return statements in arrow functions
 11 |     + for-of ......... for loop to for-of loop
 12 |     + for-each ....... for loop to Array.forEach()
 13 |     + arg-rest ....... use of arguments to function(...args)
 14 |     + arg-spread ..... use of apply() to spread operator
 15 |     + obj-method ..... function values in objects to methods
 16 |     + obj-shorthand .. {foo: foo} to {foo}
 17 |     + no-strict ...... remove "use strict" directives
 18 |     + exponent ....... Math.pow() to ** operator (ES7)
 19 |     + multi-var ...... single var x,y; declaration to var x; var y; (refactor)
 20 | 
 21 |   Unsafe transforms:
 22 | 
 23 |     + let ............ var to let/const
 24 |     + class .......... prototype assignments to class declaration
 25 |     + commonjs ....... CommonJS module loading to import/export
 26 |     + template ....... string concatenation to template string
 27 |     + default-param .. use of || to default parameters
 28 |     + destruct-param . use destructuring for objects in function parameters
 29 |     + includes ....... indexOf() != -1 to includes() (ES7)
 30 | `;
 31 | 
 32 | /**
 33 |  * Command line options parser.
 34 |  */
 35 | export default class OptionParser {
 36 |   constructor() {
 37 |     this.program = new Command();
 38 |     this.program.usage('-t <transform> <file>');
 39 |     this.program.description(`${pkg.description}\n${transformsDocs}`);
 40 |     this.program.version(pkg.version);
 41 |     this.program.option('-o, --out-file <file>', 'write output to a file');
 42 |     this.program.option('--replace <dir>', `in-place transform all *.js files in a directory
 43 |                              <dir> can also be a single file or a glob pattern`);
 44 |     this.program.option('-t, --transform <a,b,c>', 'one or more transformations to perform', v => v.split(','));
 45 |   }
 46 | 
 47 |   /**
 48 |    * Parses and validates command line options from argv.
 49 |    *
 50 |    * - On success returns object with options.
 51 |    * - On failure throws exceptions with error message to be shown to user.
 52 |    *
 53 |    * @param {String[]} argv Raw command line arguments
 54 |    * @return {Object} options object
 55 |    */
 56 |   parse(argv) {
 57 |     this.program.parse(argv);
 58 | 
 59 |     return {
 60 |       inFile: this.getInputFile(),
 61 |       outFile: this.opts().outFile,
 62 |       replace: this.getReplace(),
 63 |       transforms: this.getTransforms(),
 64 |     };
 65 |   }
 66 | 
 67 |   getInputFile() {
 68 |     if (this.program.args.length > 1) {
 69 |       throw `Only one input file allowed, but ${this.program.args.length} given instead.`;
 70 |     }
 71 |     if (this.program.args[0] && !fs.existsSync(this.program.args[0])) {
 72 |       throw `File ${this.program.args[0]} does not exist.`;
 73 |     }
 74 |     return this.program.args[0];
 75 |   }
 76 | 
 77 |   getReplace() {
 78 |     if (!this.opts().replace) {
 79 |       return undefined;
 80 |     }
 81 |     if (this.opts().outFile) {
 82 |       throw 'The --replace and --out-file options cannot be used together.';
 83 |     }
 84 |     if (this.program.args[0]) {
 85 |       throw 'The --replace and plain input file options cannot be used together.\n' +
 86 |         'Did you forget to quote the --replace parameter?';
 87 |     }
 88 |     if (fs.existsSync(this.opts().replace) && fs.statSync(this.opts().replace).isDirectory()) {
 89 |       return path.join(this.opts().replace, '/**/*.js');
 90 |     }
 91 |     return this.opts().replace;
 92 |   }
 93 | 
 94 |   getTransforms() {
 95 |     if (!this.opts().transform || this.opts().transform.length === 0) {
 96 |       throw `No transforms specified :(
 97 | 
 98 |   Use --transform option to pick one of the following:
 99 |   ${transformsDocs}`;
100 |     }
101 | 
102 |     return this.opts().transform;
103 |   }
104 | 
105 |   opts() {
106 |     return this.program.opts();
107 |   }
108 | }
109 | 


--------------------------------------------------------------------------------
/src/Parser.js:
--------------------------------------------------------------------------------
 1 | import * as espree from 'espree';
 2 | 
 3 | const ESPREE_OPTS = {
 4 |   ecmaVersion: 2022,
 5 |   ecmaFeatures: {jsx: true},
 6 |   comment: true,
 7 |   tokens: true
 8 | };
 9 | 
10 | /**
11 |  * An Esprima-compatible parser with JSX and object rest/spread parsing enabled.
12 |  */
13 | export default {
14 |   parse(js, opts) {
15 |     return espree.parse(js, {...opts, ...ESPREE_OPTS});
16 |   },
17 |   tokenize(js, opts) {
18 |     return espree.tokenize(js, {...opts, ...ESPREE_OPTS});
19 |   },
20 | };
21 | 


--------------------------------------------------------------------------------
/src/Transformer.js:
--------------------------------------------------------------------------------
 1 | import {parse, print} from 'recast';
 2 | import parser from './Parser';
 3 | import Logger from './Logger';
 4 | 
 5 | /**
 6 |  * Runs transforms on code.
 7 |  */
 8 | export default class Transformer {
 9 |   /**
10 |    * @param {Function[]} transforms List of transforms to perform
11 |    */
12 |   constructor(transforms = []) {
13 |     this.transforms = transforms;
14 |   }
15 | 
16 |   /**
17 |    * Tranforms code using all configured transforms.
18 |    *
19 |    * @param {String} code Input ES5 code
20 |    * @return {Object} Output ES6 code
21 |    */
22 |   run(code) {
23 |     const logger = new Logger();
24 | 
25 |     return {
26 |       code: this.applyAllTransforms(code, logger),
27 |       warnings: logger.getWarnings(),
28 |     };
29 |   }
30 | 
31 |   applyAllTransforms(code, logger) {
32 |     return this.ignoringHashBangComment(code, (js) => {
33 |       const ast = parse(js, {parser});
34 | 
35 |       this.transforms.forEach(transformer => {
36 |         transformer(ast.program, logger);
37 |       });
38 | 
39 |       return print(ast, {
40 |         lineTerminator: this.detectLineTerminator(code),
41 |         objectCurlySpacing: false,
42 |       }).code;
43 |     });
44 |   }
45 | 
46 |   // strips hashBang comment,
47 |   // invokes callback with normal js,
48 |   // then re-adds the hashBang comment back
49 |   ignoringHashBangComment(code, callback) {
50 |     const [/* all */, hashBang, js] = code.match(/^(\s*#!.*?\r?\n|)([\s\S]*)$/);
51 |     return hashBang + callback(js);
52 |   }
53 | 
54 |   detectLineTerminator(code) {
55 |     const hasCRLF = /\r\n/.test(code);
56 |     const hasLF = /[^\r]\n/.test(code);
57 | 
58 |     return (hasCRLF && !hasLF) ? '\r\n' : '\n';
59 |   }
60 | }
61 | 


--------------------------------------------------------------------------------
/src/createTransformer.js:
--------------------------------------------------------------------------------
 1 | import Transformer from './Transformer';
 2 | 
 3 | import classTransform from './transform/class';
 4 | import templateTransform from './transform/template';
 5 | import arrowTransform from './transform/arrow';
 6 | import arrowReturnTransform from './transform/arrowReturn';
 7 | import letTransform from './transform/let';
 8 | import defaultParamTransform from './transform/defaultParam';
 9 | import destructParamTransform from './transform/destructParam';
10 | import argSpreadTransform from './transform/argSpread';
11 | import argRestTransform from './transform/argRest';
12 | import objMethodTransform from './transform/objMethod';
13 | import objShorthandTransform from './transform/objShorthand';
14 | import noStrictTransform from './transform/noStrict';
15 | import commonjsTransform from './transform/commonjs';
16 | import exponentTransform from './transform/exponent';
17 | import multiVarTransform from './transform/multiVar';
18 | import forOfTransform from './transform/forOf';
19 | import forEachTransform from './transform/forEach';
20 | import includesTransform from './transform/includes';
21 | 
22 | const transformsMap = {
23 |   'class': classTransform,
24 |   'template': templateTransform,
25 |   'arrow': arrowTransform,
26 |   'arrow-return': arrowReturnTransform,
27 |   'let': letTransform,
28 |   'default-param': defaultParamTransform,
29 |   'destruct-param': destructParamTransform,
30 |   'arg-spread': argSpreadTransform,
31 |   'arg-rest': argRestTransform,
32 |   'obj-method': objMethodTransform,
33 |   'obj-shorthand': objShorthandTransform,
34 |   'no-strict': noStrictTransform,
35 |   'commonjs': commonjsTransform,
36 |   'exponent': exponentTransform,
37 |   'multi-var': multiVarTransform,
38 |   'for-of': forOfTransform,
39 |   'for-each': forEachTransform,
40 |   'includes': includesTransform,
41 | };
42 | 
43 | /**
44 |  * Factory for creating a Transformer
45 |  * by just specifying the names of the transforms.
46 |  * @param  {String[]} transformNames
47 |  * @return {Transformer}
48 |  */
49 | export default function createTransformer(transformNames) {
50 |   validate(transformNames);
51 |   return new Transformer(transformNames.map(name => transformsMap[name]));
52 | }
53 | 
54 | function validate(transformNames) {
55 |   transformNames.forEach(name => {
56 |     if (!transformsMap[name]) {
57 |       throw `Unknown transform "${name}".`;
58 |     }
59 |   });
60 | }
61 | 


--------------------------------------------------------------------------------
/src/io.js:
--------------------------------------------------------------------------------
 1 | import fs from 'fs';
 2 | 
 3 | // Taken from http://stackoverflow.com/questions/3430939/node-js-readsync-from-stdin/16048083#16048083
 4 | function readStdin() {
 5 |   const BUFSIZE = 256;
 6 |   const buf = 'alloc' in Buffer ? Buffer.alloc(BUFSIZE) : new Buffer(BUFSIZE);
 7 |   let bytesRead;
 8 |   let out = '';
 9 | 
10 |   do {
11 |     try {
12 |       bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE);
13 |     }
14 |     catch (e) {
15 |       if (e.code === 'EAGAIN') { // 'resource temporarily unavailable'
16 |         // Happens on OS X 10.8.3 (not Windows 7!), if there's no
17 |         // stdin input - typically when invoking a script without any
18 |         // input (for interactive stdin input).
19 |         // If you were to just continue, you'd create a tight loop.
20 |         throw e;
21 |       }
22 |       else if (e.code === 'EOF') {
23 |         // Happens on Windows 7, but not OS X 10.8.3:
24 |         // simply signals the end of *piped* stdin input.
25 |         break;
26 |       }
27 |       throw e; // unexpected exception
28 |     }
29 |     // Process the chunk read.
30 |     out += buf.toString('utf8', 0, bytesRead);
31 |   } while (bytesRead !== 0); // Loop as long as stdin input is available.
32 | 
33 |   return out;
34 | }
35 | 
36 | /**
37 |  * Input/output helpers.
38 |  */
39 | export default {
40 |   /**
41 |    * Returns the contents of an entire file.
42 |    * When no filename given, reads from STDIN.
43 |    * @param  {String} filename
44 |    * @return {String}
45 |    */
46 |   read(filename) {
47 |     if (filename) {
48 |       return fs.readFileSync(filename).toString();
49 |     }
50 |     else {
51 |       return readStdin();
52 |     }
53 |   },
54 | 
55 |   /**
56 |    * Writes the data to file.
57 |    * When no filename given, writes to STDIN.
58 |    * @param  {String} filename
59 |    * @param  {String} data
60 |    */
61 |   write(filename, data) {
62 |     if (filename) {
63 |       fs.writeFileSync(filename, data);
64 |     }
65 |     else {
66 |       process.stdout.write(data);
67 |     }
68 |   }
69 | };
70 | 


--------------------------------------------------------------------------------
/src/scope/BlockScope.js:
--------------------------------------------------------------------------------
 1 | import Scope from './Scope';
 2 | 
 3 | /**
 4 |  * Container for block-scoped variables.
 5 |  */
 6 | export default
 7 | class BlockScope extends Scope {
 8 |   /**
 9 |    * Registers variables in block scope.
10 |    *
11 |    * (All variables are first registered in function scope.)
12 |    *
13 |    * @param  {String} name Variable name
14 |    * @param  {Variable} variable Variable object
15 |    */
16 |   register(name, variable) {
17 |     if (!this.vars[name]) {
18 |       this.vars[name] = [];
19 |     }
20 |     this.vars[name].push(variable);
21 |   }
22 | 
23 |   /**
24 |    * Looks up variables from function scope.
25 |    *
26 |    * Traveling up the scope chain until reaching a function scope.
27 |    *
28 |    * @param  {String} name Variable name
29 |    * @return {Variable[]} The found variables (empty array if none found)
30 |    */
31 |   findFunctionScoped(name) {
32 |     return this.parent.findFunctionScoped(name);
33 |   }
34 | 
35 |   /**
36 |    * Looks up variables from block scope.
37 |    *
38 |    * Either from the current block, or any parent block.
39 |    * When variable found from function scope instead,
40 |    * returns empty array to signify it's not properly block-scoped.
41 |    *
42 |    * @param  {String} name Variable name
43 |    * @return {Variable[]} The found variables (empty array if none found)
44 |    */
45 |   findBlockScoped(name) {
46 |     if (this.vars[name]) {
47 |       return this.vars[name];
48 |     }
49 |     return this.parent.findBlockScoped(name);
50 |   }
51 | }
52 | 


--------------------------------------------------------------------------------
/src/scope/FunctionHoister.js:
--------------------------------------------------------------------------------
 1 | import {flow, map, flatten, forEach} from 'lodash/fp';
 2 | import traverser from '../traverser';
 3 | import * as functionType from '../utils/functionType';
 4 | import * as destructuring from '../utils/destructuring.js';
 5 | import Variable from '../scope/Variable';
 6 | import VariableGroup from '../scope/VariableGroup';
 7 | 
 8 | /**
 9 |  * Registers all variables defined inside a function.
10 |  * Emulating ECMAScript variable hoisting.
11 |  */
12 | export default
13 | class FunctionHoister {
14 |   /**
15 |    * Instantiates hoister with a function scope where to
16 |    * register the variables that are found.
17 |    * @param  {FunctionScope} functionScope
18 |    */
19 |   constructor(functionScope) {
20 |     this.functionScope = functionScope;
21 |   }
22 | 
23 |   /**
24 |    * Performs the hoisting of a function name, params and variables.
25 |    *
26 |    * @param {Object} cfg
27 |    *   @param {Identifier} cfg.id Optional function name
28 |    *   @param {Identifier[]} cfg.params Optional function parameters
29 |    *   @param {Object} cfg.body Function body node or Program node to search variables from.
30 |    */
31 |   hoist({id, params, body}) {
32 |     if (id) {
33 |       this.declareVariable(id, id.name);
34 |     }
35 |     if (params) {
36 |       this.hoistFunctionParams(params);
37 |     }
38 |     this.hoistVariables(body);
39 |   }
40 | 
41 |   hoistFunctionParams(params) {
42 |     return flow(
43 |       map(destructuring.extractVariables),
44 |       flatten,
45 |       forEach((node) => this.declareVariable(node, node.name))
46 |     )(params);
47 |   }
48 | 
49 |   declareVariable(node, name) {
50 |     const variable = new Variable(node);
51 |     variable.markDeclared();
52 |     this.functionScope.register(name, variable);
53 |   }
54 | 
55 |   hoistVariables(ast) {
56 |     traverser.traverse(ast, {
57 |       // Use arrow-function here, so we can access outer `this`.
58 |       enter: (node, parent) => {
59 |         if (node.type === 'VariableDeclaration') {
60 |           this.hoistVariableDeclaration(node, parent);
61 |         }
62 |         else if (functionType.isFunctionDeclaration(node)) {
63 |           // Register variable for the function if it has a name
64 |           if (node.id) {
65 |             this.declareVariable(node, node.id.name);
66 |           }
67 |           // Skip anything inside the nested function
68 |           return traverser.VisitorOption.Skip;
69 |         }
70 |         else if (functionType.isFunctionExpression(node)) {
71 |           // Skip anything inside the nested function
72 |           return traverser.VisitorOption.Skip;
73 |         }
74 |       }
75 |     });
76 |   }
77 | 
78 |   hoistVariableDeclaration(node, parent) {
79 |     const group = new VariableGroup(node, parent);
80 |     node.declarations.forEach(declaratorNode => {
81 |       const variable = new Variable(declaratorNode, group);
82 |       group.add(variable);
83 |       // All destructured variable names point to the same Variable instance,
84 |       // as we want to treat the destructured variables as one un-breakable
85 |       // unit - if one of them is modified and other one not, we cannot break
86 |       // them apart into const and let, but instead need to use let for both.
87 |       destructuring.extractVariableNames(declaratorNode.id).forEach(name => {
88 |         this.functionScope.register(name, variable);
89 |       });
90 |     });
91 |   }
92 | }
93 | 


--------------------------------------------------------------------------------
/src/scope/FunctionScope.js:
--------------------------------------------------------------------------------
 1 | import Scope from './Scope';
 2 | 
 3 | /**
 4 |  * Container for function-scoped variables.
 5 |  */
 6 | export default
 7 | class FunctionScope extends Scope {
 8 |   /**
 9 |    * Registers variables in function scope.
10 |    *
11 |    * All variables (including function name and params) are first
12 |    * registered as function scoped, during hoisting phase.
13 |    * Later they can also be registered in block scope.
14 |    *
15 |    * @param  {String} name Variable name
16 |    * @param  {Variable} variable Variable object
17 |    */
18 |   register(name, variable) {
19 |     if (!this.vars[name]) {
20 |       this.vars[name] = [variable];
21 |     }
22 |     this.vars[name].push(variable);
23 |   }
24 | 
25 |   /**
26 |    * Looks up variables from function scope.
27 |    * (Either from this function scope or from any parent function scope.)
28 |    *
29 |    * @param  {String} name Variable name
30 |    * @return {Variable[]} The found variables (empty array if none found)
31 |    */
32 |   findFunctionScoped(name) {
33 |     if (this.vars[name]) {
34 |       return this.vars[name];
35 |     }
36 |     if (this.parent) {
37 |       return this.parent.findFunctionScoped(name);
38 |     }
39 |     return [];
40 |   }
41 | 
42 |   /**
43 |    * Looks up variables from block scope.
44 |    * (i.e. the parent block scope of the function scope.)
45 |    *
46 |    * When variable found from function scope instead,
47 |    * returns an empty array to signify it's not properly block-scoped.
48 |    *
49 |    * @param  {String} name Variable name
50 |    * @return {Variable[]} The found variables (empty array if none found)
51 |    */
52 |   findBlockScoped(name) {
53 |     if (this.vars[name]) {
54 |       return [];
55 |     }
56 |     if (this.parent) {
57 |       return this.parent.findBlockScoped(name);
58 |     }
59 |     return [];
60 |   }
61 | }
62 | 


--------------------------------------------------------------------------------
/src/scope/Scope.js:
--------------------------------------------------------------------------------
 1 | import {values, flatten} from 'lodash/fp';
 2 | 
 3 | /**
 4 |  * Base class for Function- and BlockScope.
 5 |  *
 6 |  * Subclasses implement:
 7 |  *
 8 |  * - register() for adding variables to scope
 9 |  * - findFunctionScoped() for finding function-scoped vars
10 |  * - findBlockScoped() for finding block-scoped vars
11 |  */
12 | export default
13 | class Scope {
14 |   /**
15 |    * @param  {Scope} parent Parent scope (if any).
16 |    */
17 |   constructor(parent) {
18 |     this.parent = parent;
19 |     this.vars = Object.create(null); // eslint-disable-line no-null/no-null
20 |   }
21 | 
22 |   /**
23 |    * Returns parent scope (possibly undefined).
24 |    * @return {Scope}
25 |    */
26 |   getParent() {
27 |     return this.parent;
28 |   }
29 | 
30 |   /**
31 |    * Returns all variables registered in this scope.
32 |    * @return {Variable[]}
33 |    */
34 |   getVariables() {
35 |     return flatten(values(this.vars));
36 |   }
37 | }
38 | 


--------------------------------------------------------------------------------
/src/scope/ScopeManager.js:
--------------------------------------------------------------------------------
 1 | import BlockScope from '../scope/BlockScope';
 2 | import FunctionScope from '../scope/FunctionScope';
 3 | 
 4 | /**
 5 |  * Keeps track of the current function/block scope.
 6 |  */
 7 | export default
 8 | class ScopeManager {
 9 |   constructor() {
10 |     this.scope = undefined;
11 |   }
12 | 
13 |   /**
14 |    * Enters new function scope
15 |    */
16 |   enterFunction() {
17 |     this.scope = new FunctionScope(this.scope);
18 |   }
19 | 
20 |   /**
21 |    * Enters new block scope
22 |    */
23 |   enterBlock() {
24 |     this.scope = new BlockScope(this.scope);
25 |   }
26 | 
27 |   /**
28 |    * Leaves the current scope.
29 |    */
30 |   leaveScope() {
31 |     this.scope = this.scope.getParent();
32 |   }
33 | 
34 |   /**
35 |    * Returns the current scope.
36 |    * @return {FunctionScope|BlockScope}
37 |    */
38 |   getScope() {
39 |     return this.scope;
40 |   }
41 | }
42 | 


--------------------------------------------------------------------------------
/src/scope/Variable.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Encapsulates a single variable declaring AST node.
 3 |  *
 4 |  * It might be the actual VariableDeclarator node,
 5 |  * but it might also be a function parameter or -name.
 6 |  */
 7 | export default
 8 | class Variable {
 9 |   /**
10 |    * @param  {Object} node AST node declaring the variable.
11 |    * @param  {VariableGroup} group The containing var-statement (if any).
12 |    */
13 |   constructor(node, group) {
14 |     this.node = node;
15 |     this.group = group;
16 |     this.declared = false;
17 |     this.hoisted = false;
18 |     this.modified = false;
19 |   }
20 | 
21 |   markDeclared() {
22 |     this.declared = true;
23 |   }
24 | 
25 |   isDeclared() {
26 |     return this.declared;
27 |   }
28 | 
29 |   /**
30 |    * Marks that the use of the variable is not block-scoped,
31 |    * so it cannot be simply converted to `let` or `const`.
32 |    */
33 |   markHoisted() {
34 |     this.hoisted = true;
35 |   }
36 | 
37 |   /**
38 |    * Marks that the variable is assigned to,
39 |    * so it cannot be converted to `const`.
40 |    */
41 |   markModified() {
42 |     this.modified = true;
43 |   }
44 | 
45 |   /**
46 |    * Returns the strictest possible kind-attribute value for this variable.
47 |    *
48 |    * @return {String} Either "var", "let" or "const".
49 |    */
50 |   getKind() {
51 |     if (this.hoisted) {
52 |       return 'var';
53 |     }
54 |     else if (this.modified) {
55 |       return 'let';
56 |     }
57 |     else {
58 |       return 'const';
59 |     }
60 |   }
61 | 
62 |   /**
63 |    * Returns the AST node that declares this variable.
64 |    * @return {Object}
65 |    */
66 |   getNode() {
67 |     return this.node;
68 |   }
69 | 
70 |   /**
71 |    * Returns the containing var-statement (if any).
72 |    * @return {VariableGroup}
73 |    */
74 |   getGroup() {
75 |     return this.group;
76 |   }
77 | }
78 | 


--------------------------------------------------------------------------------
/src/scope/VariableGroup.js:
--------------------------------------------------------------------------------
 1 | import {min} from 'lodash/fp';
 2 | 
 3 | /**
 4 |  * Encapsulates a VariableDeclaration node
 5 |  * and a list of Variable objects declared by it.
 6 |  *
 7 |  * PS. Named VariableGroup not VariableDeclaration to avoid confusion with syntax class.
 8 |  */
 9 | export default
10 | class VariableGroup {
11 |   /**
12 |    * @param  {VariableDeclaration} node AST node
13 |    * @param  {Object} parentNode Parent AST node (pretty much any node)
14 |    */
15 |   constructor(node, parentNode) {
16 |     this.node = node;
17 |     this.parentNode = parentNode;
18 |     this.variables = [];
19 |   }
20 | 
21 |   /**
22 |    * Adds a variable to this group.
23 |    * @param {Variable} variable
24 |    */
25 |   add(variable) {
26 |     this.variables.push(variable);
27 |   }
28 | 
29 |   /**
30 |    * Returns all variables declared in this group.
31 |    * @return {Variable[]}
32 |    */
33 |   getVariables() {
34 |     return this.variables;
35 |   }
36 | 
37 |   /**
38 |    * Returns the `kind` value of variable defined in this group.
39 |    *
40 |    * When not all variables are of the same kind, returns undefined.
41 |    *
42 |    * @return {String} Either "var", "let", "const" or undefined.
43 |    */
44 |   getCommonKind() {
45 |     const firstKind = this.variables[0].getKind();
46 |     if (this.variables.every(v => v.getKind() === firstKind)) {
47 |       return firstKind;
48 |     }
49 |     else {
50 |       return undefined;
51 |     }
52 |   }
53 | 
54 |   /**
55 |    * Returns the most restrictive possible common `kind` value
56 |    * for variables defined in this group.
57 |    *
58 |    * - When all vars are const, return "const".
59 |    * - When some vars are "let" and some "const", returns "let".
60 |    * - When some vars are "var", return "var".
61 |    *
62 |    * @return {String} Either "var", "let" or "const".
63 |    */
64 |   getMostRestrictiveKind() {
65 |     const kindToVal = {
66 |       'var': 1,
67 |       'let': 2,
68 |       'const': 3,
69 |     };
70 |     const valToKind = {
71 |       1: 'var',
72 |       2: 'let',
73 |       3: 'const',
74 |     };
75 | 
76 |     const minVal = min(this.variables.map(v => kindToVal[v.getKind()]));
77 |     return valToKind[minVal];
78 |   }
79 | 
80 |   /**
81 |    * Returns the AST node
82 |    * @return {VariableDeclaration}
83 |    */
84 |   getNode() {
85 |     return this.node;
86 |   }
87 | 
88 |   /**
89 |    * Returns the parent AST node (which can be pretty much anything)
90 |    * @return {Object}
91 |    */
92 |   getParentNode() {
93 |     return this.parentNode;
94 |   }
95 | }
96 | 


--------------------------------------------------------------------------------
/src/scope/VariableMarker.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Labels variables in relation to their use in block scope.
  3 |  *
  4 |  * When variable is declared/modified/referenced not according to
  5 |  * block scoping rules, it'll be marked hoisted.
  6 |  */
  7 | export default
  8 | class VariableMarker {
  9 |   /**
 10 |    * @param  {ScopeManager} scopeManager
 11 |    */
 12 |   constructor(scopeManager) {
 13 |     this.scopeManager = scopeManager;
 14 |   }
 15 | 
 16 |   /**
 17 |    * Marks set of variables declared in current block scope.
 18 |    *
 19 |    * Takes an array of variable names to support the case of declaring
 20 |    * multiple variables at once with a destructuring operation.
 21 |    *
 22 |    * - Not valid block var when already declared before.
 23 |    *
 24 |    * @param  {String[]} varNames
 25 |    */
 26 |   markDeclared(varNames) {
 27 |     const alreadySeen = [];
 28 | 
 29 |     varNames.forEach(varName => {
 30 |       const blockVars = this.getScope().findFunctionScoped(varName);
 31 | 
 32 |       // all variable names declared with a destructuring operation
 33 |       // reference the same Variable object, so when we mark the
 34 |       // first variable in destructuring as declared, they all
 35 |       // will be marked as declared, but this kind of re-declaring
 36 |       // (which isn't actually real re-declaring) should not cause
 37 |       // variable to be marked as declared multiple times and
 38 |       // therefore marked as hoisted.
 39 |       if (blockVars.some(v => !alreadySeen.includes(v))) {
 40 |         alreadySeen.push(...blockVars);
 41 | 
 42 |         // Ignore repeated var declarations
 43 |         if (blockVars.some((variable) => variable.isDeclared())) {
 44 |           for (const variable of blockVars) {
 45 |             variable.markHoisted();
 46 |           }
 47 |           return;
 48 |         }
 49 |       }
 50 | 
 51 |       for (const variable of blockVars) {
 52 |         // Remember that it's declared and register in current block scope
 53 |         variable.markDeclared();
 54 |       }
 55 | 
 56 |       const scope = this.getScope();
 57 |       for (const variable of blockVars) {
 58 |         scope.register(varName, variable);
 59 |       }
 60 |     });
 61 |   }
 62 | 
 63 |   /**
 64 |    * Marks variable modified in current block scope.
 65 |    *
 66 |    * - Not valid block var when not declared in current block scope.
 67 |    *
 68 |    * @param  {String} varName
 69 |    */
 70 |   markModified(varName) {
 71 |     const blockVars = this.getScope().findBlockScoped(varName);
 72 |     if (blockVars.length > 0) {
 73 |       for (const variable of blockVars) {
 74 |         variable.markModified();
 75 |       }
 76 |       return;
 77 |     }
 78 | 
 79 |     for (const variable of this.getScope().findFunctionScoped(varName)) {
 80 |       variable.markHoisted();
 81 |       variable.markModified();
 82 |     }
 83 |   }
 84 | 
 85 |   /**
 86 |    * Marks variable referenced in current block scope.
 87 |    *
 88 |    * - Not valid block var when not declared in current block scope.
 89 |    *
 90 |    * @param  {String} varName
 91 |    */
 92 |   markReferenced(varName) {
 93 |     const blockVars = this.getScope().findBlockScoped(varName);
 94 |     if (blockVars.length > 0) {
 95 |       return;
 96 |     }
 97 | 
 98 |     for (const variable of this.getScope().findFunctionScoped(varName)) {
 99 |       variable.markHoisted();
100 |     }
101 |   }
102 | 
103 |   getScope() {
104 |     return this.scopeManager.getScope();
105 |   }
106 | }
107 | 


--------------------------------------------------------------------------------
/src/syntax/ArrowFunctionExpression.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the ArrowFunctionExpression syntax
 5 |  */
 6 | export default
 7 | class ArrowFunctionExpression extends BaseSyntax {
 8 |   /**
 9 |    * The constructor of ArrowFunctionExpression
10 |    *
11 |    * @param {Object} cfg
12 |    * @param {Node} cfg.body
13 |    * @param {Node[]} cfg.params
14 |    * @param {Node[]} cfg.defaults
15 |    * @param {Node} cfg.rest (optional)
16 |    */
17 |   constructor({body, params, defaults, rest, async}) {
18 |     super('ArrowFunctionExpression');
19 | 
20 |     this.body = body;
21 |     this.params = params;
22 |     this.defaults = defaults;
23 |     this.rest = rest;
24 |     this.async = async;
25 |     this.generator = false;
26 |     this.id = undefined;
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/src/syntax/BaseSyntax.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * @abstract BaseSyntax
 3 |  */
 4 | export default
 5 | class BaseSyntax {
 6 |   /**
 7 |    * The constructor of BaseSyntax
 8 |    *
 9 |    * @param {String} type
10 |    */
11 |   constructor(type) {
12 |     this.type = type;
13 |   }
14 | }
15 | 


--------------------------------------------------------------------------------
/src/syntax/ExportNamedDeclaration.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the ExportNamedDeclaration syntax.
 5 |  */
 6 | export default
 7 | class ExportNamedDeclaration extends BaseSyntax {
 8 |   /**
 9 |    * Constructed with either declaration or specifiers.
10 |    * @param {Object} cfg
11 |    * @param {Object} cfg.declaration Any *Declaration node (optional)
12 |    * @param {Object[]} cfg.specifiers List of specifiers (optional)
13 |    * @param {Object[]} cfg.comments Comments data (optional)
14 |    */
15 |   constructor({declaration, specifiers, comments}) {
16 |     super('ExportNamedDeclaration');
17 |     this.declaration = declaration;
18 |     this.specifiers = specifiers;
19 |     this.comments = comments;
20 |   }
21 | }
22 | 


--------------------------------------------------------------------------------
/src/syntax/ImportDeclaration.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the ImportDeclaration syntax
 5 |  */
 6 | export default
 7 | class ImportDeclaration extends BaseSyntax {
 8 |   /**
 9 |    * @param {Object} cfg
10 |    * @param {ImportSpecifier[]|ImportDefaultSpecifier[]} cfg.specifiers
11 |    * @param {Literal} cfg.source String literal containing library path
12 |    */
13 |   constructor({specifiers, source}) {
14 |     super('ImportDeclaration');
15 |     this.specifiers = specifiers;
16 |     this.source = source;
17 |   }
18 | }
19 | 


--------------------------------------------------------------------------------
/src/syntax/ImportDefaultSpecifier.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the ImportDefaultSpecifier syntax
 5 |  */
 6 | export default
 7 | class ImportDefaultSpecifier extends BaseSyntax {
 8 |   /**
 9 |    * @param {Identifier} local  The local variable where to import
10 |    */
11 |   constructor(local) {
12 |     super('ImportDefaultSpecifier');
13 |     this.local = local;
14 |   }
15 | }
16 | 


--------------------------------------------------------------------------------
/src/syntax/ImportSpecifier.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the ImportSpecifier syntax
 5 |  */
 6 | export default
 7 | class ImportSpecifier extends BaseSyntax {
 8 |   /**
 9 |    * @param {Object} cfg
10 |    * @param {Identifier} cfg.local  The local variable
11 |    * @param {Identifier} cfg.imported  The imported variable
12 |    */
13 |   constructor({local, imported}) {
14 |     super('ImportSpecifier');
15 |     this.local = local;
16 |     this.imported = imported;
17 |   }
18 | }
19 | 


--------------------------------------------------------------------------------
/src/syntax/TemplateElement.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the TemplateElement syntax
 5 |  */
 6 | export default
 7 | class TemplateElement extends BaseSyntax {
 8 |   /**
 9 |    * Create a template literal
10 |    *
11 |    * @param {Object} cfg
12 |    * @param {String} cfg.raw As it looks in source, with escapes added
13 |    * @param {String} cfg.cooked The actual value
14 |    * @param {Boolean} cfg.tail True to signify the last element in TemplateLiteral
15 |    */
16 |   constructor({raw = '', cooked = '', tail = false}) {
17 |     super('TemplateElement');
18 | 
19 |     this.value = {raw, cooked};
20 |     this.tail = tail;
21 |   }
22 | }
23 | 


--------------------------------------------------------------------------------
/src/syntax/TemplateLiteral.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the TemplateLiteral syntax
 5 |  */
 6 | export default
 7 | class TemplateLiteral extends BaseSyntax {
 8 |   /**
 9 |    * Create a template literal
10 |    * @param {Object[]} quasis String parts
11 |    * @param {Object[]} expressions Expressions between string parts
12 |    */
13 |   constructor({quasis, expressions}) {
14 |     super('TemplateLiteral');
15 | 
16 |     this.quasis = quasis;
17 |     this.expressions = expressions;
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/src/syntax/VariableDeclaration.js:
--------------------------------------------------------------------------------
 1 | import BaseSyntax from './BaseSyntax';
 2 | 
 3 | /**
 4 |  * The class to define the VariableDeclaration syntax
 5 |  */
 6 | export default
 7 | class VariableDeclaration extends BaseSyntax {
 8 |   /**
 9 |    * The constructor of VariableDeclaration
10 |    *
11 |    * @param {String} kind
12 |    * @param {VariableDeclarator[]} declarations
13 |    */
14 |   constructor(kind, declarations) {
15 |     super('VariableDeclaration');
16 | 
17 |     this.kind = kind;
18 |     this.declarations = declarations;
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/src/transform/argRest.js:
--------------------------------------------------------------------------------
 1 | import {find} from 'lodash/fp';
 2 | import traverser from '../traverser';
 3 | import withScope from '../withScope';
 4 | 
 5 | export default function(ast) {
 6 |   traverser.replace(ast, withScope(ast, {
 7 |     enter(node, parent, scope) {
 8 |       if (isES5Function(node) && node.params.length === 0) {
 9 |         const argumentsVar = find(v => v.name === 'arguments', scope.variables);
10 |         // Look through all the places where arguments is used:
11 |         // Make sure none of these has access to some already existing `args` variable
12 |         if (
13 |           argumentsVar &&
14 |           argumentsVar.references.length > 0 &&
15 |           !argumentsVar.references.some(ref => hasArgs(ref.from))
16 |         ) {
17 |           // Change all arguments --> args
18 |           argumentsVar.references.forEach(ref => {
19 |             ref.identifier.name = 'args';
20 |           });
21 |           // Change function() --> function(...args)
22 |           node.params = [createRestElement()];
23 |         }
24 |       }
25 |     },
26 |   }));
27 | }
28 | 
29 | function isES5Function(node) {
30 |   return node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression';
31 | }
32 | 
33 | function hasArgs(scope) {
34 |   if (!scope) {
35 |     return false;
36 |   }
37 |   if (scope.variables.some(v => v.name === 'args')) {
38 |     return true;
39 |   }
40 |   return hasArgs(scope.upper);
41 | }
42 | 
43 | function createRestElement() {
44 |   return {
45 |     type: 'RestElement',
46 |     argument: {
47 |       type: 'Identifier',
48 |       name: 'args'
49 |     }
50 |   };
51 | }
52 | 


--------------------------------------------------------------------------------
/src/transform/argSpread.js:
--------------------------------------------------------------------------------
  1 | import {flow, omit, mapValues, isEqual, isArray, isObjectLike} from 'lodash/fp';
  2 | import traverser from '../traverser';
  3 | import {matches, extract, extractAny} from 'f-matches';
  4 | 
  5 | export default function(ast) {
  6 |   traverser.replace(ast, {
  7 |     enter(node) {
  8 |       const {func, array} = matchFunctionApplyCall(node);
  9 |       if (func) {
 10 |         return createCallWithSpread(func, array);
 11 |       }
 12 | 
 13 |       const {memberExpr, thisParam, arrayParam} = matchObjectApplyCall(node);
 14 |       if (memberExpr && isEqual(omitLoc(memberExpr.object), omitLoc(thisParam))) {
 15 |         return createCallWithSpread(memberExpr, arrayParam);
 16 |       }
 17 |     }
 18 |   });
 19 | }
 20 | 
 21 | function createCallWithSpread(func, array) {
 22 |   return {
 23 |     type: 'CallExpression',
 24 |     callee: func,
 25 |     arguments: [
 26 |       {
 27 |         type: 'SpreadElement',
 28 |         argument: array,
 29 |       }
 30 |     ]
 31 |   };
 32 | }
 33 | 
 34 | // Recursively strips `loc`, `start` and `end` fields from given object and its nested objects,
 35 | // removing the location information that we don't care about when comparing
 36 | // AST nodes.
 37 | function omitLoc(obj) {
 38 |   if (isArray(obj)) {
 39 |     return obj.map(omitLoc);
 40 |   }
 41 |   else if (isObjectLike(obj)) {
 42 |     return flow(
 43 |       omit(['loc', 'start', 'end']),
 44 |       mapValues(omitLoc)
 45 |     )(obj);
 46 |   }
 47 |   else {
 48 |     return obj;
 49 |   }
 50 | }
 51 | 
 52 | const isUndefined = matches({
 53 |   type: 'Identifier',
 54 |   name: 'undefined'
 55 | });
 56 | 
 57 | const isNull = matches({
 58 |   type: 'Literal',
 59 |   value: null, // eslint-disable-line no-null/no-null
 60 |   raw: 'null'
 61 | });
 62 | 
 63 | function matchFunctionApplyCall(node) {
 64 |   return matches({
 65 |     type: 'CallExpression',
 66 |     callee: {
 67 |       type: 'MemberExpression',
 68 |       computed: false,
 69 |       object: extract('func', {
 70 |         type: 'Identifier'
 71 |       }),
 72 |       property: {
 73 |         type: 'Identifier',
 74 |         name: 'apply'
 75 |       }
 76 |     },
 77 |     arguments: [
 78 |       arg => isUndefined(arg) || isNull(arg),
 79 |       extractAny('array')
 80 |     ]
 81 |   }, node);
 82 | }
 83 | 
 84 | function matchObjectApplyCall(node) {
 85 |   return matches({
 86 |     type: 'CallExpression',
 87 |     callee: {
 88 |       type: 'MemberExpression',
 89 |       computed: false,
 90 |       object: extract('memberExpr', {
 91 |         type: 'MemberExpression',
 92 |       }),
 93 |       property: {
 94 |         type: 'Identifier',
 95 |         name: 'apply'
 96 |       }
 97 |     },
 98 |     arguments: [
 99 |       extractAny('thisParam'),
100 |       extractAny('arrayParam')
101 |     ]
102 |   }, node);
103 | }
104 | 


--------------------------------------------------------------------------------
/src/transform/arrow.js:
--------------------------------------------------------------------------------
  1 | import {matches as lodashMatches} from 'lodash/fp';
  2 | import traverser from '../traverser';
  3 | import ArrowFunctionExpression from '../syntax/ArrowFunctionExpression';
  4 | import {matches, matchesLength, extract} from 'f-matches';
  5 | import copyComments from '../utils/copyComments';
  6 | 
  7 | export default function(ast, logger) {
  8 |   traverser.replace(ast, {
  9 |     enter(node, parent) {
 10 |       if (isFunctionConvertableToArrow(node, parent)) {
 11 |         if (hasArguments(node.body)) {
 12 |           logger.warn(node, 'Can not use arguments in arrow function', 'arrow');
 13 |           return;
 14 |         }
 15 |         return functionToArrow(node, parent);
 16 |       }
 17 | 
 18 |       const {func} = matchBoundFunction(node);
 19 |       if (func) {
 20 |         return functionToArrow(func, parent);
 21 |       }
 22 |     }
 23 |   });
 24 | }
 25 | 
 26 | function isFunctionConvertableToArrow(node, parent) {
 27 |   return node.type === 'FunctionExpression' &&
 28 |     parent.type !== 'Property' &&
 29 |     parent.type !== 'MethodDefinition' &&
 30 |     !node.id &&
 31 |     !node.generator &&
 32 |     !hasThis(node.body);
 33 | }
 34 | 
 35 | // Matches: function(){}.bind(this)
 36 | function matchBoundFunction(node) {
 37 |   return matches({
 38 |     type: 'CallExpression',
 39 |     callee: {
 40 |       type: 'MemberExpression',
 41 |       computed: false,
 42 |       object: extract('func', {
 43 |         type: 'FunctionExpression',
 44 |         id: null, // eslint-disable-line no-null/no-null
 45 |         body: body => !hasArguments(body),
 46 |         generator: false
 47 |       }),
 48 |       property: {
 49 |         type: 'Identifier',
 50 |         name: 'bind'
 51 |       }
 52 |     },
 53 |     arguments: matchesLength([
 54 |       {
 55 |         type: 'ThisExpression'
 56 |       }
 57 |     ])
 58 |   }, node);
 59 | }
 60 | 
 61 | function hasThis(ast) {
 62 |   return hasInFunctionBody(ast, {type: 'ThisExpression'});
 63 | }
 64 | 
 65 | function hasArguments(ast) {
 66 |   return hasInFunctionBody(ast, {type: 'Identifier', name: 'arguments'});
 67 | }
 68 | 
 69 | // Returns true when pattern matches any node in given function body,
 70 | // excluding any nested functions
 71 | function hasInFunctionBody(ast, pattern) {
 72 |   return traverser.find(ast, lodashMatches(pattern), {
 73 |     skipTypes: ['FunctionExpression', 'FunctionDeclaration']
 74 |   });
 75 | }
 76 | 
 77 | function functionToArrow(func, parent) {
 78 |   const arrow = new ArrowFunctionExpression({
 79 |     body: func.body,
 80 |     params: func.params,
 81 |     defaults: func.defaults,
 82 |     rest: func.rest,
 83 |     async: func.async,
 84 |   });
 85 | 
 86 |   copyComments({from: func, to: arrow});
 87 | 
 88 |   // Get rid of extra parentheses around IIFE
 89 |   // by forcing Recast to reformat the CallExpression
 90 |   if (isIIFE(func, parent)) {
 91 |     parent.original = null; // eslint-disable-line no-null/no-null
 92 |   }
 93 | 
 94 |   return arrow;
 95 | }
 96 | 
 97 | // Is it immediately invoked function expression?
 98 | function isIIFE(func, parent) {
 99 |   return parent.type === 'CallExpression' && parent.callee === func;
100 | }
101 | 


--------------------------------------------------------------------------------
/src/transform/arrowReturn.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../traverser';
 2 | import {matches, matchesLength, extract} from 'f-matches';
 3 | import copyComments from '../utils/copyComments';
 4 | import {isNull, negate} from 'lodash/fp';
 5 | 
 6 | export default function(ast) {
 7 |   traverser.replace(ast, {
 8 |     enter(node) {
 9 |       if (isShortenableArrowFunction(node)) {
10 |         return shortenReturn(node);
11 |       }
12 |     }
13 |   });
14 | }
15 | 
16 | function shortenReturn(node) {
17 |   node.body = extractArrowBody(node.body);
18 |   return node;
19 | }
20 | 
21 | const matchesReturnBlock = matches({
22 |   type: 'BlockStatement',
23 |   body: matchesLength([
24 |     extract('returnStatement', {
25 |       type: 'ReturnStatement',
26 |       argument: extract('returnVal', negate(isNull))
27 |     })
28 |   ])
29 | });
30 | 
31 | function isShortenableArrowFunction(node) {
32 |   return node.type === 'ArrowFunctionExpression' &&
33 |     matchesReturnBlock(node.body);
34 | }
35 | 
36 | function extractArrowBody(block) {
37 |   const {returnStatement, returnVal} = matchesReturnBlock(block);
38 |   // preserve return statement comments
39 |   copyComments({from: returnStatement, to: returnVal});
40 |   return returnVal;
41 | }
42 | 


--------------------------------------------------------------------------------
/src/transform/class/PotentialClass.js:
--------------------------------------------------------------------------------
  1 | import {compact} from 'lodash/fp';
  2 | import extractComments from './extractComments';
  3 | import multiReplaceStatement from './../../utils/multiReplaceStatement';
  4 | 
  5 | /**
  6 |  * Represents a potential class to be created.
  7 |  */
  8 | export default
  9 | class PotentialClass {
 10 |   /**
 11 |    * @param {Object} cfg
 12 |    *   @param {String} cfg.name Class name
 13 |    *   @param {Object} cfg.fullNode Node to remove after converting to class
 14 |    *   @param {Object[]} cfg.commentNodes Nodes to extract comments from
 15 |    *   @param {Object} cfg.parent
 16 |    */
 17 |   constructor({name, fullNode, commentNodes, parent}) {
 18 |     this.name = name;
 19 |     this.constructor = undefined;
 20 |     this.fullNode = fullNode;
 21 |     this.superClass = undefined;
 22 |     this.commentNodes = commentNodes;
 23 |     this.parent = parent;
 24 |     this.methods = [];
 25 |     this.replacements = [];
 26 |   }
 27 | 
 28 |   /**
 29 |    * Returns the name of the class.
 30 |    * @return {String}
 31 |    */
 32 |   getName() {
 33 |     return this.name;
 34 |   }
 35 | 
 36 |   /**
 37 |    * Returns the AST node for the original function
 38 |    * @return {Object}
 39 |    */
 40 |   getFullNode() {
 41 |     return this.fullNode;
 42 |   }
 43 | 
 44 |   /**
 45 |    * Set the constructor.
 46 |    * @param {PotentialMethod} method.
 47 |    */
 48 |   setConstructor(method) {
 49 |     this.constructor = method;
 50 |   }
 51 | 
 52 |   /**
 53 |    * Set the superClass and set up the related assignment expressions to be
 54 |    * removed during transformation.
 55 |    * @param {Node} superClass           The super class node.
 56 |    * @param {Node[]} relatedExpressions The related expressions to be removed
 57 |    *                                    during transformation.
 58 |    */
 59 |   setSuperClass(superClass, relatedExpressions) {
 60 |     this.superClass = superClass;
 61 |     for (const {parent, node} of relatedExpressions) {
 62 |       this.replacements.push({
 63 |         parent,
 64 |         node,
 65 |         replacements: []
 66 |       });
 67 |     }
 68 | 
 69 |     this.constructor.setSuperClass(superClass);
 70 |   }
 71 | 
 72 |   /**
 73 |    * Adds method to class.
 74 |    * @param {PotentialMethod} method
 75 |    */
 76 |   addMethod(method) {
 77 |     this.methods.push(method);
 78 |   }
 79 | 
 80 |   /**
 81 |    * True when class has at least one method (besides constructor).
 82 |    * @return {Boolean}
 83 |    */
 84 |   isTransformable() {
 85 |     return this.methods.length > 0 || this.superClass !== undefined;
 86 |   }
 87 | 
 88 |   /**
 89 |    * Replaces original constructor function and manual prototype assignments
 90 |    * with ClassDeclaration.
 91 |    */
 92 |   transform() {
 93 |     multiReplaceStatement({
 94 |       parent: this.parent,
 95 |       node: this.fullNode,
 96 |       replacements: [this.toClassDeclaration()],
 97 |     });
 98 |     this.replacements.forEach(multiReplaceStatement);
 99 | 
100 |     this.methods.forEach(method => method.remove());
101 |   }
102 | 
103 |   toClassDeclaration() {
104 |     return {
105 |       type: 'ClassDeclaration',
106 |       superClass: this.superClass,
107 |       id: {
108 |         type: 'Identifier',
109 |         name: this.name,
110 |       },
111 |       body: {
112 |         type: 'ClassBody',
113 |         body: this.createMethods()
114 |       },
115 |       comments: extractComments(this.commentNodes),
116 |     };
117 |   }
118 | 
119 |   createMethods() {
120 |     return compact([
121 |       this.createConstructor(),
122 |       ...this.methods.map(method => {
123 |         method.setSuperClass(this.superClass);
124 |         return method.toMethodDefinition();
125 |       })
126 |     ]);
127 |   }
128 | 
129 |   createConstructor() {
130 |     return this.constructor.isEmpty() ? undefined : this.constructor.toMethodDefinition();
131 |   }
132 | }
133 | 


--------------------------------------------------------------------------------
/src/transform/class/PotentialConstructor.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../../traverser';
 2 | import isEqualAst from './../../utils/isEqualAst';
 3 | import {matches} from 'f-matches';
 4 | import PotentialMethod from './PotentialMethod';
 5 | 
 6 | /**
 7 |  * Represents a potential constructor method to be created.
 8 |  */
 9 | export default
10 | class PotentialConstructor extends PotentialMethod {
11 |   constructor(cfg) {
12 |     cfg.name = 'constructor';
13 |     super(cfg);
14 |   }
15 | 
16 |   // Override superclass method
17 |   getBody() {
18 |     if (this.superClass) {
19 |       return this.transformSuperCalls(this.getBodyBlock());
20 |     }
21 |     else {
22 |       return this.getBodyBlock();
23 |     }
24 |   }
25 | 
26 |   // Transforms constructor body by replacing
27 |   // SuperClass.call(this, ...args) --> super(...args)
28 |   transformSuperCalls(body) {
29 |     return traverser.replace(body, {
30 |       enter: (node) => {
31 |         if (this.isSuperConstructorCall(node)) {
32 |           node.expression.callee = {
33 |             type: 'Super'
34 |           };
35 |           node.expression.arguments = node.expression.arguments.slice(1);
36 |         }
37 |       }
38 |     });
39 |   }
40 | 
41 |   isSuperConstructorCall(node) {
42 |     return matches({
43 |       type: 'ExpressionStatement',
44 |       expression: {
45 |         type: 'CallExpression',
46 |         callee: {
47 |           type: 'MemberExpression',
48 |           object: obj => isEqualAst(obj, this.superClass),
49 |           property: {
50 |             type: 'Identifier',
51 |             name: 'call'
52 |           }
53 |         },
54 |         arguments: [
55 |           {
56 |             type: 'ThisExpression'
57 |           }
58 |         ]
59 |       }
60 |     }, node);
61 |   }
62 | }
63 | 


--------------------------------------------------------------------------------
/src/transform/class/PotentialMethod.js:
--------------------------------------------------------------------------------
  1 | import traverser from '../../traverser';
  2 | import isEqualAst from './../../utils/isEqualAst';
  3 | import {matches, extract} from 'f-matches';
  4 | import extractComments from './extractComments';
  5 | import multiReplaceStatement from './../../utils/multiReplaceStatement';
  6 | 
  7 | /**
  8 |  * Represents a potential class method to be created.
  9 |  */
 10 | export default
 11 | class PotentialMethod {
 12 |   /**
 13 |    * @param {Object} cfg
 14 |    *   @param {String} cfg.name Method name
 15 |    *   @param {Object} cfg.methodNode
 16 |    *   @param {Object} cfg.fullNode Node to remove after converting to class
 17 |    *   @param {Object[]} cfg.commentNodes Nodes to extract comments from
 18 |    *   @param {Object} cfg.parent
 19 |    *   @param {String} [cfg.kind] Either 'get' or 'set' (optional)
 20 |    *   @param {Boolean} [cfg.static] True to make static method (optional)
 21 |    */
 22 |   constructor(cfg) {
 23 |     this.name = cfg.name;
 24 |     this.methodNode = cfg.methodNode;
 25 |     this.fullNode = cfg.fullNode;
 26 |     this.commentNodes = cfg.commentNodes || [];
 27 |     this.parent = cfg.parent;
 28 |     this.kind = cfg.kind || 'method';
 29 |     this.static = cfg.static || false;
 30 |   }
 31 | 
 32 |   /**
 33 |    * Sets the superClass node.
 34 |    * @param {Node} superClass
 35 |    */
 36 |   setSuperClass(superClass) {
 37 |     this.superClass = superClass;
 38 |   }
 39 | 
 40 |   /**
 41 |    * True when method body is empty.
 42 |    * @return {Boolean}
 43 |    */
 44 |   isEmpty() {
 45 |     return this.getBodyBlock().body.length === 0;
 46 |   }
 47 | 
 48 |   /**
 49 |    * Transforms the potential method to actual MethodDefinition node.
 50 |    * @return {MethodDefinition}
 51 |    */
 52 |   toMethodDefinition() {
 53 |     return {
 54 |       type: 'MethodDefinition',
 55 |       key: {
 56 |         type: 'Identifier',
 57 |         name: this.name,
 58 |       },
 59 |       computed: false,
 60 |       value: {
 61 |         type: 'FunctionExpression',
 62 |         async: this.methodNode.async,
 63 |         params: this.methodNode.params,
 64 |         defaults: this.methodNode.defaults,
 65 |         body: this.getBody(),
 66 |         generator: false,
 67 |         expression: false,
 68 |       },
 69 |       kind: this.kind,
 70 |       static: this.static,
 71 |       comments: extractComments(this.commentNodes),
 72 |     };
 73 |   }
 74 | 
 75 |   /**
 76 |    * Removes original prototype assignment node from AST.
 77 |    */
 78 |   remove() {
 79 |     multiReplaceStatement({
 80 |       parent: this.parent,
 81 |       node: this.fullNode,
 82 |       replacements: [],
 83 |     });
 84 |   }
 85 | 
 86 |   // To be overridden in subclasses
 87 |   getBody() {
 88 |     if (this.superClass) {
 89 |       return this.transformSuperCalls(this.getBodyBlock());
 90 |     }
 91 |     else {
 92 |       return this.getBodyBlock();
 93 |     }
 94 |   }
 95 | 
 96 |   getBodyBlock() {
 97 |     if (this.methodNode.body.type === 'BlockStatement') {
 98 |       return this.methodNode.body;
 99 |     }
100 |     else {
101 |       return {
102 |         type: 'BlockStatement',
103 |         body: [
104 |           {
105 |             type: 'ReturnStatement',
106 |             argument: this.methodNode.body
107 |           }
108 |         ]
109 |       };
110 |     }
111 |   }
112 | 
113 |   // Transforms method body by replacing
114 |   // SuperClass.prototype.foo.call(this, ...args) --> super.foo(...args)
115 |   transformSuperCalls(body) {
116 |     return traverser.replace(body, {
117 |       enter: (node) => {
118 |         const m = this.matchSuperCall(node);
119 |         if (m) {
120 |           node.expression.callee = {
121 |             type: 'MemberExpression',
122 |             computed: false,
123 |             object: {
124 |               type: 'Super'
125 |             },
126 |             property: m.method
127 |           };
128 | 
129 |           node.expression.arguments = node.expression.arguments.slice(1);
130 |         }
131 |       }
132 |     });
133 |   }
134 | 
135 |   matchSuperCall(node) {
136 |     return matches({
137 |       type: 'ExpressionStatement',
138 |       expression: {
139 |         type: 'CallExpression',
140 |         callee: {
141 |           type: 'MemberExpression',
142 |           computed: false,
143 |           object: {
144 |             type: 'MemberExpression',
145 |             computed: false,
146 |             object: {
147 |               type: 'MemberExpression',
148 |               computed: false,
149 |               object: (obj) => isEqualAst(obj, this.superClass),
150 |               property: {
151 |                 type: 'Identifier',
152 |                 name: 'prototype'
153 |               }
154 |             },
155 |             property: extract('method', {
156 |               type: 'Identifier',
157 |             })
158 |           },
159 |           property: {
160 |             type: 'Identifier',
161 |             name: 'call'
162 |           }
163 |         },
164 |         arguments: [
165 |           {
166 |             type: 'ThisExpression'
167 |           }
168 |         ]
169 |       }
170 |     }, node);
171 |   }
172 | }
173 | 


--------------------------------------------------------------------------------
/src/transform/class/extractComments.js:
--------------------------------------------------------------------------------
 1 | import {flatMap} from 'lodash/fp';
 2 | /**
 3 |  * Extracts comments from an array of nodes.
 4 |  * @param {Object[]} nodes
 5 |  * @param {Object[]} extracted comments
 6 |  */
 7 | export default function extractComments(nodes) {
 8 |   return flatMap(n => n.comments || [], nodes);
 9 | }
10 | 


--------------------------------------------------------------------------------
/src/transform/class/index.js:
--------------------------------------------------------------------------------
  1 | import {values} from 'lodash/fp';
  2 | import traverser from '../../traverser';
  3 | import PotentialClass from './PotentialClass';
  4 | import PotentialMethod from './PotentialMethod';
  5 | import PotentialConstructor from './PotentialConstructor';
  6 | import matchFunctionDeclaration from './matchFunctionDeclaration';
  7 | import matchFunctionVar from './matchFunctionVar';
  8 | import matchFunctionAssignment from './matchFunctionAssignment';
  9 | import matchPrototypeFunctionAssignment from './matchPrototypeFunctionAssignment';
 10 | import matchPrototypeObjectAssignment from './matchPrototypeObjectAssignment';
 11 | import matchObjectDefinePropertyCall, {isAccessorDescriptor} from './matchObjectDefinePropertyCall';
 12 | import Inheritance from './inheritance/Inheritance';
 13 | import matchObjectDefinePropertiesCall, {matchDefinedProperties} from './matchObjectDefinePropertiesCall.js';
 14 | 
 15 | export default function(ast, logger) {
 16 |   const potentialClasses = {};
 17 |   const inheritance = new Inheritance();
 18 | 
 19 |   traverser.traverse(ast, {
 20 |     enter(node, parent) {
 21 |       let m;
 22 | 
 23 |       if ((m = matchFunctionDeclaration(node) || matchFunctionVar(node))) {
 24 |         potentialClasses[m.className] = new PotentialClass({
 25 |           name: m.className,
 26 |           fullNode: node,
 27 |           commentNodes: [node],
 28 |           parent,
 29 |         });
 30 |         potentialClasses[m.className].setConstructor(
 31 |           new PotentialConstructor({
 32 |             methodNode: m.constructorNode,
 33 |             potentialClass: potentialClasses[m.className]
 34 |           })
 35 |         );
 36 |       }
 37 |       else if ((m = matchFunctionAssignment(node))) {
 38 |         if (potentialClasses[m.className]) {
 39 |           potentialClasses[m.className].addMethod(new PotentialMethod({
 40 |             name: m.methodName,
 41 |             methodNode: m.methodNode,
 42 |             fullNode: node,
 43 |             commentNodes: [node],
 44 |             parent,
 45 |             static: true,
 46 |           }));
 47 |         }
 48 |       }
 49 |       else if ((m = matchPrototypeFunctionAssignment(node))) {
 50 |         if (potentialClasses[m.className]) {
 51 |           potentialClasses[m.className].addMethod(new PotentialMethod({
 52 |             name: m.methodName,
 53 |             methodNode: m.methodNode,
 54 |             fullNode: node,
 55 |             commentNodes: [node],
 56 |             parent,
 57 |           }));
 58 |         }
 59 |       }
 60 |       else if ((m = matchPrototypeObjectAssignment(node))) {
 61 |         if (potentialClasses[m.className]) {
 62 |           m.methods.forEach((method, i) => {
 63 |             const assignmentComments = (i === 0) ? [node] : [];
 64 | 
 65 |             potentialClasses[m.className].addMethod(new PotentialMethod({
 66 |               name: method.methodName,
 67 |               methodNode: method.methodNode,
 68 |               fullNode: node,
 69 |               commentNodes: assignmentComments.concat([method.propertyNode]),
 70 |               parent,
 71 |               kind: classMethodKind(method.kind),
 72 |             }));
 73 |           });
 74 |         }
 75 |       }
 76 |       else if ((m = matchObjectDefinePropertyCall(node))) {
 77 |         if (potentialClasses[m.className]) {
 78 |           m.descriptors.forEach((desc, i) => {
 79 |             const parentComments = (i === 0) ? [node] : [];
 80 | 
 81 |             potentialClasses[m.className].addMethod(new PotentialMethod({
 82 |               name: m.methodName,
 83 |               methodNode: desc.methodNode,
 84 |               fullNode: node,
 85 |               commentNodes: parentComments.concat([desc.propertyNode]),
 86 |               parent,
 87 |               kind: desc.kind,
 88 |               static: m.static
 89 |             }));
 90 |           });
 91 |         }
 92 |       }
 93 |       else if ((m = matchObjectDefinePropertiesCall(node))) {
 94 |         // defineProperties allows mixing method definitions we CAN transform
 95 |         // with ones we CANT. This check looks for whether every property is
 96 |         // one we CAN transform and if they are it removes the whole call
 97 |         // to defineProperties
 98 |         let removeWholeNode = false;
 99 |         if (node.expression.arguments[1].properties.every(propertyNode => {
100 |           const {properties} = matchDefinedProperties(propertyNode);
101 |           return properties.some(isAccessorDescriptor);
102 |         })) {
103 |           removeWholeNode = true;
104 |         }
105 | 
106 |         m.forEach((method, i) => {
107 |           if (potentialClasses[method.className]) {
108 |             method.descriptors.forEach((desc, j) => {
109 |               const parentComments = (j === 0) ? [method.methodNode] : [];
110 | 
111 |               // by default remove only the single method property of the object passed to defineProperties
112 |               // otherwise if they should all be remove AND this is the last descriptor set it up
113 |               // to remove the whole node that's calling defineProperties
114 |               const lastDescriptor = i === m.length - 1 && j === method.descriptors.length - 1;
115 |               const fullNode = lastDescriptor && removeWholeNode ? node : method.methodNode;
116 |               const parentNode = lastDescriptor && removeWholeNode ? parent : node.expression.arguments[1];
117 | 
118 |               potentialClasses[method.className].addMethod(new PotentialMethod({
119 |                 name: method.methodName,
120 |                 methodNode: desc.methodNode,
121 |                 fullNode: fullNode,
122 |                 commentNodes: parentComments.concat([desc.propertyNode]),
123 |                 parent: parentNode,
124 |                 kind: desc.kind,
125 |                 static: method.static
126 |               }));
127 |             });
128 |           }
129 |         });
130 |       }
131 |       else if ((m = inheritance.process(node, parent))) {
132 |         if (potentialClasses[m.className]) {
133 |           potentialClasses[m.className].setSuperClass(
134 |             m.superClass,
135 |             m.relatedExpressions
136 |           );
137 |         }
138 |       }
139 |     },
140 |     leave(node) {
141 |       if (node.type === 'Program') {
142 |         values(potentialClasses)
143 |           .filter(cls => cls.isTransformable() ? true : logWarning(cls))
144 |           .forEach(cls => cls.transform());
145 |       }
146 |     }
147 |   });
148 | 
149 |   // Ordinary methods inside class use kind=method,
150 |   // unlike methods inside object literal, which use kind=init.
151 |   function classMethodKind(kind) {
152 |     return kind === 'init' ? 'method' : kind;
153 |   }
154 | 
155 |   function logWarning(cls) {
156 |     if (/^[A-Z]/.test(cls.getName())) {
157 |       logger.warn(
158 |         cls.getFullNode(),
159 |         `Function ${cls.getName()} looks like class, but has no prototype`,
160 |         'class'
161 |       );
162 |     }
163 |   }
164 | }
165 | 


--------------------------------------------------------------------------------
/src/transform/class/inheritance/ImportUtilDetector.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny, matchesLength} from 'f-matches';
 2 | 
 3 | /**
 4 |  * Detects variable name imported from: import <name> from "util"
 5 |  */
 6 | export default class ImportUtilDetector {
 7 |   /**
 8 |    * Detects: import <identifier> from "util"
 9 |    *
10 |    * @param {Object} node
11 |    * @return {Object} MemberExpression of <identifier>.inherits
12 |    */
13 |   detect(node) {
14 |     const m = this.matchImportUtil(node);
15 |     if (m) {
16 |       return {
17 |         type: 'MemberExpression',
18 |         computed: false,
19 |         object: {
20 |           type: 'Identifier',
21 |           name: m.name,
22 |         },
23 |         property: {
24 |           type: 'Identifier',
25 |           name: 'inherits'
26 |         }
27 |       };
28 |     }
29 |   }
30 | 
31 |   // Matches: import <name> from "util"
32 |   matchImportUtil(node) {
33 |     return matches({
34 |       type: 'ImportDeclaration',
35 |       specifiers: matchesLength([
36 |         {
37 |           type: 'ImportDefaultSpecifier',
38 |           local: {
39 |             type: 'Identifier',
40 |             name: extractAny('name')
41 |           }
42 |         }
43 |       ]),
44 |       source: {
45 |         type: 'Literal',
46 |         value: 'util'
47 |       }
48 |     }, node);
49 |   }
50 | }
51 | 


--------------------------------------------------------------------------------
/src/transform/class/inheritance/Inheritance.js:
--------------------------------------------------------------------------------
 1 | import UtilInherits from './UtilInherits';
 2 | import Prototypal from './Prototypal';
 3 | 
 4 | /**
 5 |  * Processes nodes to detect super classes and return information for later
 6 |  * transformation.
 7 |  */
 8 | export default class Inheritance {
 9 |   /**
10 |    * @param {Object} cfg
11 |    *   @param {PotentialClass[]} cfg.potentialClasses Class name
12 |    */
13 |   constructor() {
14 |     this.utilInherits = new UtilInherits();
15 |     this.prototypal = new Prototypal();
16 |   }
17 | 
18 |   /**
19 |    * Process a node and return inheritance details if found.
20 |    * @param {Object} node
21 |    * @param {Object} parent
22 |    * @returns {Object}
23 |    *            {String}   className
24 |    *            {Node}     superClass
25 |    *            {Object[]} relatedExpressions
26 |    */
27 |   process(node, parent) {
28 |     return (
29 |       this.utilInherits.process(node, parent) ||
30 |       this.prototypal.process(node, parent)
31 |     );
32 |   }
33 | }
34 | 


--------------------------------------------------------------------------------
/src/transform/class/inheritance/Prototypal.js:
--------------------------------------------------------------------------------
  1 | import {matches, matchesLength, extractAny} from 'f-matches';
  2 | 
  3 | /**
  4 |  * Processes nodes to detect super classes and return information for later
  5 |  * transformation.
  6 |  *
  7 |  * Detects:
  8 |  *
  9 |  *   Class1.prototype = Object.create(Class2.prototype);
 10 |  *
 11 |  * or:
 12 |  *
 13 |  *   Class1.prototype = new Class2();
 14 |  *
 15 |  * optionally followed by:
 16 |  *
 17 |  *   Class1.prototype.constructor = Class1;
 18 |  */
 19 | export default class Prototypal {
 20 |   constructor() {
 21 |     this.foundSuperclasses = {};
 22 |   }
 23 | 
 24 |   /**
 25 |    * Process a node and return inheritance details if found.
 26 |    * @param {Object} node
 27 |    * @param {Object} parent
 28 |    * @returns {Object/undefined} m
 29 |    *                    {String}   m.className
 30 |    *                    {Node}     m.superClass
 31 |    *                    {Object[]} m.relatedExpressions
 32 |    */
 33 |   process(node, parent) {
 34 |     let m;
 35 |     if ((m = this.matchNewAssignment(node) || this.matchObjectCreateAssignment(node))) {
 36 |       this.foundSuperclasses[m.className] = m.superClass;
 37 | 
 38 |       return {
 39 |         className: m.className,
 40 |         superClass: m.superClass,
 41 |         relatedExpressions: [
 42 |           {node, parent},
 43 |         ]
 44 |       };
 45 |     }
 46 |     else if ((m = this.matchConstructorAssignment(node))) {
 47 |       const superClass = this.foundSuperclasses[m.className];
 48 |       if (superClass && m.className === m.constructorClassName) {
 49 |         return {
 50 |           className: m.className,
 51 |           superClass: superClass,
 52 |           relatedExpressions: [
 53 |             {node, parent},
 54 |           ]
 55 |         };
 56 |       }
 57 |     }
 58 |   }
 59 | 
 60 |   // Matches: <className>.prototype = new <superClass>();
 61 |   matchNewAssignment(node) {
 62 |     return matches({
 63 |       type: 'ExpressionStatement',
 64 |       expression: {
 65 |         type: 'AssignmentExpression',
 66 |         left: {
 67 |           type: 'MemberExpression',
 68 |           object: {
 69 |             type: 'Identifier',
 70 |             name: extractAny('className')
 71 |           },
 72 |           property: {
 73 |             type: 'Identifier',
 74 |             name: 'prototype'
 75 |           }
 76 |         },
 77 |         right: {
 78 |           type: 'NewExpression',
 79 |           callee: extractAny('superClass')
 80 |         }
 81 |       }
 82 |     }, node);
 83 |   }
 84 | 
 85 |   // Matches: <className>.prototype = Object.create(<superClass>);
 86 |   matchObjectCreateAssignment(node) {
 87 |     return matches({
 88 |       type: 'ExpressionStatement',
 89 |       expression: {
 90 |         type: 'AssignmentExpression',
 91 |         left: {
 92 |           type: 'MemberExpression',
 93 |           object: {
 94 |             type: 'Identifier',
 95 |             name: extractAny('className')
 96 |           },
 97 |           property: {
 98 |             type: 'Identifier',
 99 |             name: 'prototype'
100 |           }
101 |         },
102 |         right: {
103 |           type: 'CallExpression',
104 |           callee: {
105 |             type: 'MemberExpression',
106 |             object: {
107 |               type: 'Identifier',
108 |               name: 'Object'
109 |             },
110 |             property: {
111 |               type: 'Identifier',
112 |               name: 'create'
113 |             }
114 |           },
115 |           arguments: matchesLength([{
116 |             type: 'MemberExpression',
117 |             object: extractAny('superClass'),
118 |             property: {
119 |               type: 'Identifier',
120 |               name: 'prototype'
121 |             }
122 |           }])
123 |         }
124 |       }
125 |     }, node);
126 |   }
127 | 
128 |   // Matches: <className>.prototype.constructor = <constructorClassName>;
129 |   matchConstructorAssignment(node) {
130 |     return matches({
131 |       type: 'ExpressionStatement',
132 |       expression: {
133 |         type: 'AssignmentExpression',
134 |         left: {
135 |           type: 'MemberExpression',
136 |           object: {
137 |             type: 'MemberExpression',
138 |             object: {
139 |               type: 'Identifier',
140 |               name: extractAny('className')
141 |             },
142 |             property: {
143 |               type: 'Identifier',
144 |               name: 'prototype'
145 |             }
146 |           },
147 |           property: {
148 |             type: 'Identifier',
149 |             name: 'constructor'
150 |           }
151 |         },
152 |         right: {
153 |           type: 'Identifier',
154 |           name: extractAny('constructorClassName')
155 |         }
156 |       }
157 |     }, node);
158 |   }
159 | }
160 | 


--------------------------------------------------------------------------------
/src/transform/class/inheritance/RequireUtilDetector.js:
--------------------------------------------------------------------------------
 1 | import {find} from 'lodash/fp';
 2 | import {matches, matchesLength} from 'f-matches';
 3 | 
 4 | /**
 5 |  * Detects variable name imported from require("util")
 6 |  */
 7 | export default class RequireUtilDetector {
 8 |   /**
 9 |    * Detects: var <identifier> = require("util")
10 |    *
11 |    * @param {Object} node
12 |    * @return {Object} MemberExpression of <identifier>.inherits
13 |    */
14 |   detect(node) {
15 |     if (node.type !== 'VariableDeclaration') {
16 |       return;
17 |     }
18 | 
19 |     const declaration = find(dec => this.isRequireUtil(dec), node.declarations);
20 |     if (declaration) {
21 |       return {
22 |         type: 'MemberExpression',
23 |         computed: false,
24 |         object: {
25 |           type: 'Identifier',
26 |           name: declaration.id.name,
27 |         },
28 |         property: {
29 |           type: 'Identifier',
30 |           name: 'inherits'
31 |         }
32 |       };
33 |     }
34 |   }
35 | 
36 |   // Matches: <id> = require("util")
37 |   isRequireUtil(dec) {
38 |     return matches({
39 |       type: 'VariableDeclarator',
40 |       id: {
41 |         type: 'Identifier',
42 |       },
43 |       init: {
44 |         type: 'CallExpression',
45 |         callee: {
46 |           type: 'Identifier',
47 |           name: 'require'
48 |         },
49 |         arguments: matchesLength([{
50 |           type: 'Literal',
51 |           value: 'util'
52 |         }])
53 |       }
54 |     }, dec);
55 |   }
56 | }
57 | 


--------------------------------------------------------------------------------
/src/transform/class/inheritance/RequireUtilInheritsDetector.js:
--------------------------------------------------------------------------------
 1 | import {find} from 'lodash/fp';
 2 | import {matches, matchesLength} from 'f-matches';
 3 | 
 4 | /**
 5 |  * Detects variable name imported from require("util").inherits
 6 |  */
 7 | export default class RequireUtilInheritsDetector {
 8 |   /**
 9 |    * Detects: var <identifier> = require("util").inherits
10 |    *
11 |    * @param {Object} node
12 |    * @return {Object} Identifier
13 |    */
14 |   detect(node) {
15 |     if (node.type !== 'VariableDeclaration') {
16 |       return;
17 |     }
18 | 
19 |     const declaration = find(dec => this.isRequireUtilInherits(dec), node.declarations);
20 |     if (declaration) {
21 |       return {
22 |         type: 'Identifier',
23 |         name: declaration.id.name,
24 |       };
25 |     }
26 |   }
27 | 
28 |   // Matches: <id> = require("util").inherits
29 |   isRequireUtilInherits(dec) {
30 |     return matches({
31 |       type: 'VariableDeclarator',
32 |       id: {
33 |         type: 'Identifier',
34 |       },
35 |       init: {
36 |         type: 'MemberExpression',
37 |         computed: false,
38 |         object: {
39 |           type: 'CallExpression',
40 |           callee: {
41 |             type: 'Identifier',
42 |             name: 'require'
43 |           },
44 |           arguments: matchesLength([{
45 |             type: 'Literal',
46 |             value: 'util'
47 |           }])
48 |         },
49 |         property: {
50 |           type: 'Identifier',
51 |           name: 'inherits'
52 |         }
53 |       }
54 |     }, dec);
55 |   }
56 | }
57 | 


--------------------------------------------------------------------------------
/src/transform/class/inheritance/UtilInherits.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny} from 'f-matches';
 2 | import RequireUtilDetector from './RequireUtilDetector';
 3 | import RequireUtilInheritsDetector from './RequireUtilInheritsDetector';
 4 | import ImportUtilDetector from './ImportUtilDetector';
 5 | 
 6 | /**
 7 |  * Processes nodes to detect super classes and return information for later
 8 |  * transformation.
 9 |  *
10 |  * Detects:
11 |  *
12 |  *   var util = require('util');
13 |  *   ...
14 |  *   util.inherits(Class1, Class2);
15 |  */
16 | export default class UtilInherits {
17 |   constructor() {
18 |     this.inheritsNode = undefined;
19 |     this.detectors = [
20 |       new RequireUtilDetector(),
21 |       new RequireUtilInheritsDetector(),
22 |       new ImportUtilDetector(),
23 |     ];
24 |   }
25 | 
26 |   /**
27 |    * Process a node and return inheritance details if found.
28 |    * @param {Object} node
29 |    * @param {Object} parent
30 |    * @returns {Object/undefined} m
31 |    *                    {String}   m.className
32 |    *                    {Node}     m.superClass
33 |    *                    {Object[]} m.relatedExpressions
34 |    */
35 |   process(node, parent) {
36 |     let m;
37 |     if (parent && parent.type === 'Program' && (m = this.detectInheritsNode(node))) {
38 |       this.inheritsNode = m;
39 |     }
40 |     else if (this.inheritsNode && (m = this.matchUtilInherits(node))) {
41 |       return {
42 |         className: m.className,
43 |         superClass: m.superClass,
44 |         relatedExpressions: [{node, parent}]
45 |       };
46 |     }
47 |   }
48 | 
49 |   detectInheritsNode(node) {
50 |     for (const detector of this.detectors) {
51 |       let inheritsNode;
52 |       if ((inheritsNode = detector.detect(node))) {
53 |         return inheritsNode;
54 |       }
55 |     }
56 |   }
57 | 
58 |   // Discover usage of this.inheritsNode
59 |   //
60 |   // Matches: <this.utilInherits>(<className>, <superClass>);
61 |   matchUtilInherits(node) {
62 |     return matches({
63 |       type: 'ExpressionStatement',
64 |       expression: {
65 |         type: 'CallExpression',
66 |         callee: this.inheritsNode,
67 |         arguments: [
68 |           {
69 |             type: 'Identifier',
70 |             name: extractAny('className')
71 |           },
72 |           extractAny('superClass')
73 |         ]
74 |       }
75 |     }, node);
76 |   }
77 | }
78 | 


--------------------------------------------------------------------------------
/src/transform/class/isFunctionProperty.js:
--------------------------------------------------------------------------------
 1 | import {matches} from 'f-matches';
 2 | import isTransformableToMethod from './isTransformableToMethod';
 3 | 
 4 | /**
 5 |  * Matches: <ident>: function() { ... }
 6 |  *
 7 |  * @param {Object} node
 8 |  * @return {Boolean}
 9 |  */
10 | export default matches({
11 |   type: 'Property',
12 |   key: {
13 |     type: 'Identifier',
14 |     // name: <ident>
15 |   },
16 |   computed: false,
17 |   value: isTransformableToMethod,
18 | });
19 | 


--------------------------------------------------------------------------------
/src/transform/class/isTransformableToMethod.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../../traverser';
 2 | 
 3 | /**
 4 |  * Detects if function can be transformed to class method
 5 |  * @param  {Object}  node
 6 |  * @return {Boolean}
 7 |  */
 8 | export default function isTransformableToMethod(node) {
 9 |   if (node.type === 'FunctionExpression') {
10 |     return true;
11 |   }
12 |   if (node.type === 'ArrowFunctionExpression' && !usesThis(node)) {
13 |     return true;
14 |   }
15 | }
16 | 
17 | function usesThis(ast) {
18 |   return traverser.find(ast, 'ThisExpression', {
19 |     skipTypes: ['FunctionExpression', 'FunctionDeclaration']
20 |   });
21 | }
22 | 


--------------------------------------------------------------------------------
/src/transform/class/matchFunctionAssignment.js:
--------------------------------------------------------------------------------
 1 | import {matches, extract, extractAny} from 'f-matches';
 2 | 
 3 | /**
 4 |  * Matches: <className>.<methodName> = function () { ... }
 5 |  *
 6 |  * When node matches returns the extracted fields:
 7 |  *
 8 |  * - className
 9 |  * - methodName
10 |  * - methodNode
11 |  *
12 |  * @param  {Object} node
13 |  * @return {Object}
14 |  */
15 | export default matches({
16 |   type: 'ExpressionStatement',
17 |   expression: {
18 |     type: 'AssignmentExpression',
19 |     left: {
20 |       type: 'MemberExpression',
21 |       computed: false,
22 |       object: {
23 |         type: 'Identifier',
24 |         name: extractAny('className')
25 |       },
26 |       property: {
27 |         type: 'Identifier',
28 |         name: extractAny('methodName')
29 |       }
30 |     },
31 |     operator: '=',
32 |     right: extract('methodNode', {
33 |       type: 'FunctionExpression'
34 |     })
35 |   }
36 | });
37 | 


--------------------------------------------------------------------------------
/src/transform/class/matchFunctionDeclaration.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Matches: function <className>() { ... }
 3 |  *
 4 |  * When node matches returns the extracted fields:
 5 |  *
 6 |  * - className
 7 |  * - constructorNode
 8 |  *
 9 |  * @param  {Object} node
10 |  * @return {Object}
11 |  */
12 | export default function(node) {
13 |   if (node.type === 'FunctionDeclaration' && node.id) {
14 |     return {
15 |       className: node.id.name,
16 |       constructorNode: node
17 |     };
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/src/transform/class/matchFunctionVar.js:
--------------------------------------------------------------------------------
 1 | import {matches} from 'f-matches';
 2 | 
 3 | const isFunctionExpression = matches({
 4 |   type: 'FunctionExpression'
 5 | });
 6 | 
 7 | const isFunctionVariableDeclaration = matches({
 8 |   type: 'VariableDeclaration',
 9 |   declarations: (decs) => decs.length === 1 && isFunctionExpression(decs[0].init)
10 | });
11 | 
12 | /**
13 |  * Matches: var <className> = function () { ... }
14 |  *
15 |  * When node matches returns the extracted fields:
16 |  *
17 |  * - className
18 |  * - constructorNode
19 |  *
20 |  * @param  {Object} node
21 |  * @return {Object}
22 |  */
23 | export default function(node) {
24 |   if (isFunctionVariableDeclaration(node)) {
25 |     const {
26 |       declarations: [
27 |         {
28 |           id: {name: className},
29 |           init: constructorNode,
30 |         }
31 |       ]
32 |     } = node;
33 | 
34 |     return {className, constructorNode};
35 |   }
36 | }
37 | 


--------------------------------------------------------------------------------
/src/transform/class/matchObjectDefinePropertiesCall.js:
--------------------------------------------------------------------------------
  1 | import {matches, extractAny} from 'f-matches';
  2 | import {isAccessorDescriptor} from './matchObjectDefinePropertyCall.js';
  3 | 
  4 | const matchObjectDefinePropertiesCallOnPrototype = matches({
  5 |   type: 'ExpressionStatement',
  6 |   expression: {
  7 |     type: 'CallExpression',
  8 |     callee: {
  9 |       type: 'MemberExpression',
 10 |       computed: false,
 11 |       object: {
 12 |         type: 'Identifier',
 13 |         name: 'Object'
 14 |       },
 15 |       property: {
 16 |         type: 'Identifier',
 17 |         name: 'defineProperties'
 18 |       }
 19 |     },
 20 |     arguments: [
 21 |       {
 22 |         type: 'MemberExpression',
 23 |         computed: false,
 24 |         object: {
 25 |           type: 'Identifier',
 26 |           name: extractAny('className')
 27 |         },
 28 |         property: {
 29 |           type: 'Identifier',
 30 |           name: 'prototype'
 31 |         }
 32 |       },
 33 |       {
 34 |         type: 'ObjectExpression',
 35 |         properties: extractAny('methods')
 36 |       }
 37 |     ]
 38 |   }
 39 | });
 40 | 
 41 | const matchObjectDefinePropertiesCall = matches({
 42 |   type: 'ExpressionStatement',
 43 |   expression: {
 44 |     type: 'CallExpression',
 45 |     callee: {
 46 |       type: 'MemberExpression',
 47 |       computed: false,
 48 |       object: {
 49 |         type: 'Identifier',
 50 |         name: 'Object'
 51 |       },
 52 |       property: {
 53 |         type: 'Identifier',
 54 |         name: 'defineProperties'
 55 |       }
 56 |     },
 57 |     arguments: [
 58 |       {
 59 |         type: 'Identifier',
 60 |         name: extractAny('className')
 61 |       },
 62 |       {
 63 |         type: 'ObjectExpression',
 64 |         properties: extractAny('methods')
 65 |       }
 66 |     ]
 67 |   }
 68 | });
 69 | 
 70 | export const matchDefinedProperties = matches({
 71 |   type: 'Property',
 72 |   key: {
 73 |     type: 'Identifier',
 74 |     name: extractAny('methodName')
 75 |   },
 76 |   computed: false,
 77 |   value: {
 78 |     type: 'ObjectExpression',
 79 |     properties: extractAny('properties')
 80 |   }
 81 | });
 82 | 
 83 | 
 84 | /**
 85 |  * Matches: Object.defineProperties(<className>.prototype, {
 86 |  *              <methodName>: {
 87 |  *                <kind>: <methodNode>,
 88 |  *              }
 89 |  *              ...
 90 |  *          })
 91 |  *
 92 |  * When node matches returns an array of objects with the extracted fields for each method:
 93 |  *
 94 |  * [{
 95 |  *   - className
 96 |  *   - methodName
 97 |  *   - descriptors:
 98 |  *       - propertyNode
 99 |  *       - methodNode
100 |  *       - kind
101 |  * }]
102 |  *
103 |  * @param  {Object} node
104 |  * @return {Object[] | undefined}
105 |  */
106 | export default function(node) {
107 |   let {className, methods} = matchObjectDefinePropertiesCallOnPrototype(node);
108 | 
109 |   let isStatic = false;
110 |   if (!className) {
111 |     ({className, methods} = matchObjectDefinePropertiesCall(node));
112 |     isStatic = true;
113 |   }
114 | 
115 |   if (className) {
116 |     return methods.map(methodNode => {
117 |       const {methodName, properties} = matchDefinedProperties(methodNode);
118 | 
119 |       return {
120 |         className: className,
121 |         methodName: methodName,
122 |         methodNode: methodNode,
123 |         descriptors: properties.filter(isAccessorDescriptor).map(prop => {
124 |           return {
125 |             propertyNode: prop,
126 |             methodNode: prop.value,
127 |             kind: prop.key.name,
128 |           };
129 |         }),
130 |         static: isStatic
131 |       };
132 |     });
133 |   }
134 | }
135 | 


--------------------------------------------------------------------------------
/src/transform/class/matchObjectDefinePropertyCall.js:
--------------------------------------------------------------------------------
  1 | import {matches, extractAny} from 'f-matches';
  2 | import isFunctionProperty from './isFunctionProperty';
  3 | 
  4 | const matchObjectDefinePropertyCallOnPrototype = matches({
  5 |   type: 'ExpressionStatement',
  6 |   expression: {
  7 |     type: 'CallExpression',
  8 |     callee: {
  9 |       type: 'MemberExpression',
 10 |       computed: false,
 11 |       object: {
 12 |         type: 'Identifier',
 13 |         name: 'Object'
 14 |       },
 15 |       property: {
 16 |         type: 'Identifier',
 17 |         name: 'defineProperty'
 18 |       }
 19 |     },
 20 |     arguments: [
 21 |       {
 22 |         type: 'MemberExpression',
 23 |         computed: false,
 24 |         object: {
 25 |           type: 'Identifier',
 26 |           name: extractAny('className')
 27 |         },
 28 |         property: {
 29 |           type: 'Identifier',
 30 |           name: 'prototype'
 31 |         }
 32 |       },
 33 |       {
 34 |         type: 'Literal',
 35 |         value: extractAny('methodName')
 36 |       },
 37 |       {
 38 |         type: 'ObjectExpression',
 39 |         properties: extractAny('properties')
 40 |       }
 41 |     ]
 42 |   }
 43 | });
 44 | 
 45 | const matchObjectDefinePropertyCall = matches({
 46 |   type: 'ExpressionStatement',
 47 |   expression: {
 48 |     type: 'CallExpression',
 49 |     callee: {
 50 |       type: 'MemberExpression',
 51 |       computed: false,
 52 |       object: {
 53 |         type: 'Identifier',
 54 |         name: 'Object'
 55 |       },
 56 |       property: {
 57 |         type: 'Identifier',
 58 |         name: 'defineProperty'
 59 |       }
 60 |     },
 61 |     arguments: [
 62 |       {
 63 |         type: 'Identifier',
 64 |         name: extractAny('className')
 65 |       },
 66 |       {
 67 |         type: 'Literal',
 68 |         value: extractAny('methodName')
 69 |       },
 70 |       {
 71 |         type: 'ObjectExpression',
 72 |         properties: extractAny('properties')
 73 |       }
 74 |     ]
 75 |   }
 76 | });
 77 | 
 78 | export function isAccessorDescriptor(node) {
 79 |   return isFunctionProperty(node) &&
 80 |     (node.key.name === 'get' || node.key.name === 'set');
 81 | }
 82 | 
 83 | /**
 84 |  * Matches: Object.defineProperty(<className>.prototype, "<methodName>", {
 85 |  *              <kind>: <methodNode>,
 86 |  *              ...
 87 |  *          })
 88 |  *
 89 |  * When node matches returns the extracted fields:
 90 |  *
 91 |  * - className
 92 |  * - methodName
 93 |  * - descriptors:
 94 |  *     - propertyNode
 95 |  *     - methodNode
 96 |  *     - kind
 97 |  *
 98 |  * @param  {Object} node
 99 |  * @return {Object}
100 |  */
101 | export default function(node) {
102 |   let {className, methodName, properties} = matchObjectDefinePropertyCallOnPrototype(node);
103 | 
104 |   let isStatic = false;
105 |   if (!className) {
106 |     ({className, methodName, properties} = matchObjectDefinePropertyCall(node));
107 |     isStatic = true;
108 |   }
109 | 
110 |   if (className) {
111 |     return {
112 |       className: className,
113 |       methodName: methodName,
114 |       descriptors: properties.filter(isAccessorDescriptor).map(prop => {
115 |         return {
116 |           propertyNode: prop,
117 |           methodNode: prop.value,
118 |           kind: prop.key.name,
119 |         };
120 |       }),
121 |       static: isStatic
122 |     };
123 |   }
124 | }
125 | 


--------------------------------------------------------------------------------
/src/transform/class/matchPrototypeFunctionAssignment.js:
--------------------------------------------------------------------------------
 1 | import {matches, extract, extractAny} from 'f-matches';
 2 | import isTransformableToMethod from './isTransformableToMethod';
 3 | 
 4 | /**
 5 |  * Matches: <className>.prototype.<methodName> = function () { ... }
 6 |  *
 7 |  * When node matches returns the extracted fields:
 8 |  *
 9 |  * - className
10 |  * - methodName
11 |  * - methodNode
12 |  *
13 |  * @param  {Object} node
14 |  * @return {Object}
15 |  */
16 | export default matches({
17 |   type: 'ExpressionStatement',
18 |   expression: {
19 |     type: 'AssignmentExpression',
20 |     left: {
21 |       type: 'MemberExpression',
22 |       computed: false,
23 |       object: {
24 |         type: 'MemberExpression',
25 |         computed: false,
26 |         object: {
27 |           type: 'Identifier',
28 |           name: extractAny('className')
29 |         },
30 |         property: {
31 |           type: 'Identifier',
32 |           name: 'prototype'
33 |         },
34 |       },
35 |       property: {
36 |         type: 'Identifier',
37 |         name: extractAny('methodName')
38 |       }
39 |     },
40 |     operator: '=',
41 |     right: extract('methodNode', isTransformableToMethod)
42 |   }
43 | });
44 | 


--------------------------------------------------------------------------------
/src/transform/class/matchPrototypeObjectAssignment.js:
--------------------------------------------------------------------------------
 1 | import {matches, extract, extractAny} from 'f-matches';
 2 | import isFunctionProperty from './isFunctionProperty';
 3 | 
 4 | const matchPrototypeObjectAssignment = matches({
 5 |   type: 'ExpressionStatement',
 6 |   expression: {
 7 |     type: 'AssignmentExpression',
 8 |     left: {
 9 |       type: 'MemberExpression',
10 |       computed: false,
11 |       object: {
12 |         type: 'Identifier',
13 |         name: extractAny('className')
14 |       },
15 |       property: {
16 |         type: 'Identifier',
17 |         name: 'prototype'
18 |       },
19 |     },
20 |     operator: '=',
21 |     right: {
22 |       type: 'ObjectExpression',
23 |       properties: extract('properties', props => props.every(isFunctionProperty))
24 |     }
25 |   }
26 | });
27 | 
28 | /**
29 |  * Matches: <className>.prototype = {
30 |  *              <methodName>: <methodNode>,
31 |  *              ...
32 |  *          };
33 |  *
34 |  * When node matches returns the extracted fields:
35 |  *
36 |  * - className
37 |  * - methods
38 |  *     - propertyNode
39 |  *     - methodName
40 |  *     - methodNode
41 |  *     - kind
42 |  *
43 |  * @param  {Object} node
44 |  * @return {Object}
45 |  */
46 | export default function(node) {
47 |   const {className, properties} = matchPrototypeObjectAssignment(node);
48 | 
49 |   if (className) {
50 |     return {
51 |       className: className,
52 |       methods: properties.map(prop => {
53 |         return {
54 |           propertyNode: prop,
55 |           methodName: prop.key.name,
56 |           methodNode: prop.value,
57 |           kind: prop.kind,
58 |         };
59 |       })
60 |     };
61 |   }
62 | }
63 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/exportCommonjs.js:
--------------------------------------------------------------------------------
  1 | import traverser from '../../traverser';
  2 | import matchDefaultExport from './matchDefaultExport';
  3 | import matchNamedExport from './matchNamedExport';
  4 | import {isFunctionExpression} from '../../utils/functionType';
  5 | import ExportNamedDeclaration from '../../syntax/ExportNamedDeclaration';
  6 | import VariableDeclaration from '../../syntax/VariableDeclaration';
  7 | 
  8 | export default function(ast, logger) {
  9 |   traverser.replace(ast, {
 10 |     enter(node, parent) {
 11 |       let m;
 12 |       if ((m = matchDefaultExport(node))) {
 13 |         if (parent.type !== 'Program') {
 14 |           logger.warn(node, 'export can only be at root level', 'commonjs');
 15 |           return;
 16 |         }
 17 |         return exportDefault(m, node.comments);
 18 |       }
 19 |       else if ((m = matchNamedExport(node))) {
 20 |         if (parent.type !== 'Program') {
 21 |           logger.warn(node, 'export can only be at root level', 'commonjs');
 22 |           return;
 23 |         }
 24 |         return exportNamed(m, node.comments);
 25 |       }
 26 |     }
 27 |   });
 28 | }
 29 | 
 30 | function exportDefault({value}, comments) {
 31 |   return {
 32 |     type: 'ExportDefaultDeclaration',
 33 |     declaration: value,
 34 |     comments,
 35 |   };
 36 | }
 37 | 
 38 | function exportNamed({id, value}, comments) {
 39 |   if (isFunctionExpression(value)) {
 40 |     // Exclude functions with different name than the assigned property name
 41 |     if (compatibleIdentifiers(id, value.id)) {
 42 |       return new ExportNamedDeclaration({
 43 |         declaration: functionExpressionToDeclaration(value, id),
 44 |         comments,
 45 |       });
 46 |     }
 47 |   }
 48 |   else if (value.type === 'ClassExpression') {
 49 |     // Exclude classes with different name than the assigned property name
 50 |     if (compatibleIdentifiers(id, value.id)) {
 51 |       return new ExportNamedDeclaration({
 52 |         declaration: classExpressionToDeclaration(value, id),
 53 |         comments,
 54 |       });
 55 |     }
 56 |   }
 57 |   else if (value.type === 'Identifier') {
 58 |     return new ExportNamedDeclaration({
 59 |       specifiers: [
 60 |         {
 61 |           type: 'ExportSpecifier',
 62 |           exported: id,
 63 |           local: value
 64 |         }
 65 |       ],
 66 |       comments,
 67 |     });
 68 |   }
 69 |   else {
 70 |     return new ExportNamedDeclaration({
 71 |       declaration: new VariableDeclaration('var', [
 72 |         {
 73 |           type: 'VariableDeclarator',
 74 |           id: id,
 75 |           init: value
 76 |         }
 77 |       ]),
 78 |       comments,
 79 |     });
 80 |   }
 81 | }
 82 | 
 83 | // True when one of the identifiers is null or their names are equal.
 84 | function compatibleIdentifiers(id1, id2) {
 85 |   return !id1 || !id2 || id1.name === id2.name;
 86 | }
 87 | 
 88 | function functionExpressionToDeclaration(func, id) {
 89 |   func.type = 'FunctionDeclaration';
 90 |   func.id = id;
 91 | 
 92 |   // Transform <expression> to { return <expression>; }
 93 |   if (func.body.type !== 'BlockStatement') {
 94 |     func.body = {
 95 |       type: 'BlockStatement',
 96 |       body: [
 97 |         {
 98 |           type: 'ReturnStatement',
 99 |           argument: func.body
100 |         }
101 |       ]
102 |     };
103 |   }
104 | 
105 |   return func;
106 | }
107 | 
108 | function classExpressionToDeclaration(cls, id) {
109 |   cls.type = 'ClassDeclaration';
110 |   cls.id = id;
111 |   return cls;
112 | }
113 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/importCommonjs.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../../traverser';
 2 | import isVarWithRequireCalls from './isVarWithRequireCalls';
 3 | import {matchRequire, matchRequireWithProperty} from './matchRequire';
 4 | import multiReplaceStatement from '../../utils/multiReplaceStatement';
 5 | import ImportDeclaration from '../../syntax/ImportDeclaration';
 6 | import ImportSpecifier from '../../syntax/ImportSpecifier';
 7 | import ImportDefaultSpecifier from '../../syntax/ImportDefaultSpecifier';
 8 | import VariableDeclaration from '../../syntax/VariableDeclaration';
 9 | 
10 | export default function(ast, logger) {
11 |   traverser.replace(ast, {
12 |     enter(node, parent) {
13 |       if (isVarWithRequireCalls(node)) {
14 |         if (parent.type !== 'Program') {
15 |           logger.warn(node, 'import can only be at root level', 'commonjs');
16 |           return;
17 |         }
18 | 
19 |         multiReplaceStatement({
20 |           parent,
21 |           node,
22 |           replacements: node.declarations.map(dec => varToImport(dec, node.kind)),
23 |           preserveComments: true,
24 |         });
25 |       }
26 |     }
27 |   });
28 | }
29 | 
30 | // Converts VariableDeclarator to ImportDeclaration when we recognize it
31 | // as such, otherwise converts it to full VariableDeclaration (of original kind).
32 | function varToImport(dec, kind) {
33 |   let m;
34 |   if ((m = matchRequire(dec))) {
35 |     if (m.id.type === 'ObjectPattern') {
36 |       return patternToNamedImport(m);
37 |     }
38 |     else if (m.id.type === 'Identifier') {
39 |       return identifierToDefaultImport(m);
40 |     }
41 |   }
42 |   else if ((m = matchRequireWithProperty(dec))) {
43 |     if (m.property.name === 'default') {
44 |       return identifierToDefaultImport(m);
45 |     }
46 |     return propertyToNamedImport(m);
47 |   }
48 |   else {
49 |     return new VariableDeclaration(kind, [dec]);
50 |   }
51 | }
52 | 
53 | function patternToNamedImport({id, sources}) {
54 |   return new ImportDeclaration({
55 |     specifiers: id.properties.map(({key, value}) => {
56 |       return createImportSpecifier({
57 |         local: value,
58 |         imported: key
59 |       });
60 |     }),
61 |     source: sources[0]
62 |   });
63 | }
64 | 
65 | function identifierToDefaultImport({id, sources}) {
66 |   return new ImportDeclaration({
67 |     specifiers: [new ImportDefaultSpecifier(id)],
68 |     source: sources[0],
69 |   });
70 | }
71 | 
72 | function propertyToNamedImport({id, property, sources}) {
73 |   return new ImportDeclaration({
74 |     specifiers: [createImportSpecifier({local: id, imported: property})],
75 |     source: sources[0],
76 |   });
77 | }
78 | 
79 | function createImportSpecifier({local, imported}) {
80 |   if (imported.name === 'default') {
81 |     return new ImportDefaultSpecifier(local);
82 |   }
83 |   return new ImportSpecifier({local, imported});
84 | }
85 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/index.js:
--------------------------------------------------------------------------------
1 | import importCommonjs from './importCommonjs';
2 | import exportCommonjs from './exportCommonjs';
3 | 
4 | export default function(ast, logger) {
5 |   importCommonjs(ast, logger);
6 |   exportCommonjs(ast, logger);
7 | }
8 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/isExports.js:
--------------------------------------------------------------------------------
 1 | import {matches} from 'f-matches';
 2 | 
 3 | /**
 4 |  * Matches just identifier `exports`
 5 |  * @param  {Object} node
 6 |  * @return {Boolean}
 7 |  */
 8 | export default matches({
 9 |   type: 'Identifier',
10 |   name: 'exports'
11 | });
12 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/isModuleExports.js:
--------------------------------------------------------------------------------
 1 | import {matches} from 'f-matches';
 2 | import isExports from './isExports';
 3 | 
 4 | /**
 5 |  * Matches: module.exports
 6 |  * @param  {Object} node
 7 |  * @return {Boolean}
 8 |  */
 9 | export default matches({
10 |   type: 'MemberExpression',
11 |   computed: false,
12 |   object: {
13 |     type: 'Identifier',
14 |     name: 'module'
15 |   },
16 |   property: isExports
17 | });
18 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/isVarWithRequireCalls.js:
--------------------------------------------------------------------------------
 1 | import {matchRequire, matchRequireWithProperty} from './matchRequire';
 2 | 
 3 | /**
 4 |  * Matches: var <id> = require(<source>);
 5 |  *          var <id> = require(<source>).<property>;
 6 |  */
 7 | export default function isVarWithRequireCalls(node) {
 8 |   return node.type === 'VariableDeclaration' &&
 9 |     node.declarations.some(dec => matchRequire(dec) || matchRequireWithProperty(dec));
10 | }
11 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/matchDefaultExport.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny} from 'f-matches';
 2 | import isModuleExports from './isModuleExports';
 3 | 
 4 | /**
 5 |  * Matches: module.exports = <value>
 6 |  *
 7 |  * When match found, return object with:
 8 |  *
 9 |  * - value
10 |  *
11 |  * @param  {Object} node
12 |  * @return {Object|Boolean}
13 |  */
14 | export default matches({
15 |   type: 'ExpressionStatement',
16 |   expression: {
17 |     type: 'AssignmentExpression',
18 |     operator: '=',
19 |     left: isModuleExports,
20 |     right: extractAny('value')
21 |   },
22 | });
23 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/matchNamedExport.js:
--------------------------------------------------------------------------------
 1 | import {matches, extract, extractAny} from 'f-matches';
 2 | import isExports from './isExports';
 3 | import isModuleExports from './isModuleExports';
 4 | 
 5 | /**
 6 |  * Matches: exports.<id> = <value>
 7 |  * Matches: module.exports.<id> = <value>
 8 |  *
 9 |  * When match found, returns object with:
10 |  *
11 |  * - id
12 |  * - value
13 |  *
14 |  * @param  {[type]} node [description]
15 |  * @return {[type]}      [description]
16 |  */
17 | export default matches({
18 |   type: 'ExpressionStatement',
19 |   expression: {
20 |     type: 'AssignmentExpression',
21 |     operator: '=',
22 |     left: {
23 |       type: 'MemberExpression',
24 |       computed: false,
25 |       object: (ast) => isExports(ast) || isModuleExports(ast),
26 |       property: extract('id', {
27 |         type: 'Identifier'
28 |       })
29 |     },
30 |     right: extractAny('value')
31 |   }
32 | });
33 | 


--------------------------------------------------------------------------------
/src/transform/commonjs/matchRequire.js:
--------------------------------------------------------------------------------
 1 | import isString from '../../utils/isString';
 2 | import {matches, extract} from 'f-matches';
 3 | 
 4 | const isIdentifier = matches({
 5 |   type: 'Identifier'
 6 | });
 7 | 
 8 | // matches Property with Identifier key and value (possibly shorthand)
 9 | const isSimpleProperty = matches({
10 |   type: 'Property',
11 |   key: isIdentifier,
12 |   computed: false,
13 |   value: isIdentifier
14 | });
15 | 
16 | // matches: {a, b: myB, c, ...}
17 | const isObjectPattern = matches({
18 |   type: 'ObjectPattern',
19 |   properties: (props) => props.every(isSimpleProperty)
20 | });
21 | 
22 | // matches: require(<source>)
23 | const matchRequireCall = matches({
24 |   type: 'CallExpression',
25 |   callee: {
26 |     type: 'Identifier',
27 |     name: 'require'
28 |   },
29 |   arguments: extract('sources', (args) => {
30 |     return args.length === 1 && isString(args[0]);
31 |   })
32 | });
33 | 
34 | /**
35 |  * Matches: <id> = require(<source>);
36 |  */
37 | export const matchRequire = matches({
38 |   type: 'VariableDeclarator',
39 |   id: extract('id', id => isIdentifier(id) || isObjectPattern(id)),
40 |   init: matchRequireCall
41 | });
42 | 
43 | /**
44 |  * Matches: <id> = require(<source>).<property>;
45 |  */
46 | export const matchRequireWithProperty = matches({
47 |   type: 'VariableDeclarator',
48 |   id: extract('id', isIdentifier),
49 |   init: {
50 |     type: 'MemberExpression',
51 |     computed: false,
52 |     object: matchRequireCall,
53 |     property: extract('property', {
54 |       type: 'Identifier'
55 |     })
56 |   }
57 | });
58 | 


--------------------------------------------------------------------------------
/src/transform/defaultParam/index.js:
--------------------------------------------------------------------------------
 1 | import {matches} from 'f-matches';
 2 | import {flow, flatMap, some} from 'lodash/fp';
 3 | import {extractVariables} from '../../utils/destructuring';
 4 | import traverser from '../../traverser';
 5 | import multiReplaceStatement from '../../utils/multiReplaceStatement';
 6 | import {isFunction} from '../../utils/functionType';
 7 | import matchOrAssignment from './matchOrAssignment';
 8 | import matchTernaryAssignment from './matchTernaryAssignment';
 9 | import matchIfUndefinedAssignment from './matchIfUndefinedAssignment';
10 | 
11 | export default function(ast) {
12 |   traverser.replace(ast, {
13 |     enter(node) {
14 |       if (isFunction(node) && node.body.type === 'BlockStatement') {
15 |         transformDefaultParams(node);
16 |       }
17 |     }
18 |   });
19 | }
20 | 
21 | function transformDefaultParams(fn) {
22 |   const detectedDefaults = findDefaults(fn.body.body);
23 | 
24 |   fn.params = fn.params.map((param, i) => {
25 |     // Ignore params that use destructuring or already have a default
26 |     if (param.type !== 'Identifier') {
27 |       return param;
28 |     }
29 | 
30 |     const detected = detectedDefaults[param.name];
31 |     // Transform when default value detected
32 |     // and default does not contain this or any of the remaining parameters
33 |     if (detected && !containsParams(detected.value, remainingParams(fn, i))) {
34 |       multiReplaceStatement({
35 |         parent: fn.body,
36 |         node: detected.node,
37 |         replacements: []
38 |       });
39 |       return withDefault(param, detected.value);
40 |     }
41 | 
42 |     return param;
43 |   });
44 | }
45 | 
46 | function withDefault(param, value) {
47 |   return {
48 |     type: 'AssignmentPattern',
49 |     left: param,
50 |     right: value,
51 |   };
52 | }
53 | 
54 | function remainingParams(fn, i) {
55 |   return fn.params.slice(i);
56 | }
57 | 
58 | function containsParams(defaultValue, params) {
59 |   return flow(
60 |     flatMap(extractVariables),
61 |     some(param => traverser.find(defaultValue, matches({
62 |       type: 'Identifier',
63 |       name: param.name,
64 |     })))
65 |   )(params);
66 | }
67 | 
68 | // Looks default value assignments at the beginning of a function
69 | //
70 | // Returns a map of variable-name:{name, value, node}
71 | function findDefaults(fnBody) {
72 |   const defaults = {};
73 |   for (const node of fnBody) {
74 |     const def = matchDefaultAssignment(node);
75 |     if (!def) {
76 |       break;
77 |     }
78 |     defaults[def.name] = def;
79 |   }
80 | 
81 |   return defaults;
82 | }
83 | 
84 | function matchDefaultAssignment(node) {
85 |   return matchOrAssignment(node) ||
86 |     matchTernaryAssignment(node) ||
87 |     matchIfUndefinedAssignment(node);
88 | }
89 | 


--------------------------------------------------------------------------------
/src/transform/defaultParam/matchIfUndefinedAssignment.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny} from 'f-matches';
 2 | 
 3 | const matchEqualsUndefined = matches({
 4 |   type: 'BinaryExpression',
 5 |   left: {
 6 |     type: 'Identifier',
 7 |     name: extractAny('name2')
 8 |   },
 9 |   operator: extractAny('operator'),
10 |   right: {
11 |     type: 'Identifier',
12 |     name: 'undefined'
13 |   }
14 | });
15 | 
16 | const matchTypeofUndefined = matches({
17 |   type: 'BinaryExpression',
18 |   left: {
19 |     type: 'UnaryExpression',
20 |     operator: 'typeof',
21 |     prefix: true,
22 |     argument: {
23 |       type: 'Identifier',
24 |       name: extractAny('name2')
25 |     }
26 |   },
27 |   operator: extractAny('operator'),
28 |   right: {
29 |     type: 'Literal',
30 |     value: 'undefined'
31 |   }
32 | });
33 | 
34 | const matchIfUndefinedAssignment = matches({
35 |   type: 'ExpressionStatement',
36 |   expression: {
37 |     type: 'AssignmentExpression',
38 |     left: {
39 |       type: 'Identifier',
40 |       name: extractAny('name')
41 |     },
42 |     operator: '=',
43 |     right: {
44 |       type: 'ConditionalExpression',
45 |       test: (ast) => matchEqualsUndefined(ast) || matchTypeofUndefined(ast),
46 |       consequent: extractAny('consequent'),
47 |       alternate: extractAny('alternate')
48 |     }
49 |   }
50 | });
51 | 
52 | function isEquals(operator) {
53 |   return operator === '===' || operator === '==';
54 | }
55 | 
56 | function isNotEquals(operator) {
57 |   return operator === '!==' || operator === '!=';
58 | }
59 | 
60 | function isIdent(node, name) {
61 |   return node.type === 'Identifier' && node.name === name;
62 | }
63 | 
64 | /**
65 |  * Matches: <name> = <name> === undefined ? <value> : <name>;
66 |  * Matches: <name> = typeof <name> === 'undefined' ? <value> : <name>;
67 |  *
68 |  * When node matches returns the extracted fields:
69 |  *
70 |  * - name
71 |  * - value
72 |  * - node (the entire node)
73 |  *
74 |  * @param {Object} node
75 |  * @return {Object}
76 |  */
77 | export default function(node) {
78 |   const {name, name2, operator, consequent, alternate} = matchIfUndefinedAssignment(node) || {};
79 | 
80 |   if (name && name === name2) {
81 |     if (isEquals(operator) && isIdent(alternate, name)) {
82 |       return {name, value: consequent, node};
83 |     }
84 |     if (isNotEquals(operator) && isIdent(consequent, name)) {
85 |       return {name, value: alternate, node};
86 |     }
87 |   }
88 | }
89 | 


--------------------------------------------------------------------------------
/src/transform/defaultParam/matchOrAssignment.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny} from 'f-matches';
 2 | 
 3 | const matchOrAssignment = matches({
 4 |   type: 'ExpressionStatement',
 5 |   expression: {
 6 |     type: 'AssignmentExpression',
 7 |     left: {
 8 |       type: 'Identifier',
 9 |       name: extractAny('name')
10 |     },
11 |     operator: '=',
12 |     right: {
13 |       type: 'LogicalExpression',
14 |       left: {
15 |         type: 'Identifier',
16 |         name: extractAny('name2')
17 |       },
18 |       operator: '||',
19 |       right: extractAny('value')
20 |     }
21 |   }
22 | });
23 | 
24 | /**
25 |  * Matches: <name> = <name> || <value>;
26 |  *
27 |  * When node matches returns the extracted fields:
28 |  *
29 |  * - name
30 |  * - value
31 |  * - node (the entire node)
32 |  *
33 |  * @param {Object} node
34 |  * @return {Object}
35 |  */
36 | export default function(node) {
37 |   const {name, name2, value} = matchOrAssignment(node) || {};
38 | 
39 |   if (name && name === name2) {
40 |     return {name, value, node};
41 |   }
42 | }
43 | 


--------------------------------------------------------------------------------
/src/transform/defaultParam/matchTernaryAssignment.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny} from 'f-matches';
 2 | 
 3 | const matchTernaryAssignment = matches({
 4 |   type: 'ExpressionStatement',
 5 |   expression: {
 6 |     type: 'AssignmentExpression',
 7 |     left: {
 8 |       type: 'Identifier',
 9 |       name: extractAny('name')
10 |     },
11 |     operator: '=',
12 |     right: {
13 |       type: 'ConditionalExpression',
14 |       test: {
15 |         type: 'Identifier',
16 |         name: extractAny('name2')
17 |       },
18 |       consequent: {
19 |         type: 'Identifier',
20 |         name: extractAny('name3')
21 |       },
22 |       alternate: extractAny('value')
23 |     }
24 |   }
25 | });
26 | 
27 | /**
28 |  * Matches: <name> = <name> ? <name> : <value>;
29 |  *
30 |  * When node matches returns the extracted fields:
31 |  *
32 |  * - name
33 |  * - value
34 |  * - node (the entire node)
35 |  *
36 |  * @param {Object} node
37 |  * @return {Object}
38 |  */
39 | export default function(node) {
40 |   const {name, name2, name3, value} = matchTernaryAssignment(node) || {};
41 | 
42 |   if (name && name === name2 && name === name3) {
43 |     return {name, value, node};
44 |   }
45 | }
46 | 


--------------------------------------------------------------------------------
/src/transform/destructParam.js:
--------------------------------------------------------------------------------
  1 | import {uniq} from 'lodash/fp';
  2 | import {parse} from 'recast';
  3 | import parser from '../Parser';
  4 | import traverser from '../traverser';
  5 | import withScope from '../withScope';
  6 | import * as functionType from '../utils/functionType';
  7 | import Hierarchy from '../utils/Hierarchy';
  8 | 
  9 | const MAX_PROPS = 4;
 10 | 
 11 | export default function(ast, logger) {
 12 |   const hierarchy = new Hierarchy(ast);
 13 | 
 14 |   traverser.traverse(ast, withScope(ast, {
 15 |     enter(fnNode, parent, scope) {
 16 |       if (functionType.isFunction(fnNode)) {
 17 |         scope.variables
 18 |           .filter(isParameter)
 19 |           .map(v => ({variable: v, exs: getMemberExpressions(v, hierarchy)}))
 20 |           .filter(({exs}) => exs.length > 0)
 21 |           .forEach(({variable, exs}) => {
 22 |             // Replace parameter with destruct-pattern
 23 |             const index = fnNode.params.findIndex(param => param === variable.defs[0].name);
 24 |             if (index === -1) {
 25 |               return;
 26 |             }
 27 | 
 28 |             if (uniqPropNames(exs).length > MAX_PROPS) {
 29 |               logger.warn(
 30 |                 fnNode,
 31 |                 `${uniqPropNames(exs).length} different props found, will not transform more than ${MAX_PROPS}`,
 32 |                 'destruct-param'
 33 |               );
 34 |               return;
 35 |             }
 36 | 
 37 |             fnNode.params[index] = createDestructPattern(exs);
 38 | 
 39 |             // Replace references of obj.foo with simply foo
 40 |             exs.forEach(ex => {
 41 |               ex.type = ex.property.type;
 42 |               ex.name = ex.property.name;
 43 |             });
 44 |           });
 45 |       }
 46 |     }
 47 |   }));
 48 | }
 49 | 
 50 | function isParameter(variable) {
 51 |   return variable.defs.length === 1 && variable.defs[0].type === 'Parameter';
 52 | }
 53 | 
 54 | function getMemberExpressions(variable, hierarchy) {
 55 |   const memberExpressions = [];
 56 |   for (const ref of variable.references) {
 57 |     const memEx = hierarchy.getParent(ref.identifier);
 58 |     if (!isMemberExpressionObject(memEx, ref.identifier)) {
 59 |       return [];
 60 |     }
 61 | 
 62 |     const ex = hierarchy.getParent(memEx);
 63 |     if (isAssignment(ex, memEx) || isUpdate(ex, memEx) || isMethodCall(ex, memEx)) {
 64 |       return [];
 65 |     }
 66 | 
 67 |     if (isKeyword(memEx.property.name) || variableExists(memEx.property.name, ref.from)) {
 68 |       return [];
 69 |     }
 70 | 
 71 |     memberExpressions.push(memEx);
 72 |   }
 73 |   return memberExpressions;
 74 | }
 75 | 
 76 | function isMemberExpressionObject(memEx, object) {
 77 |   return memEx.type === 'MemberExpression' &&
 78 |     memEx.object === object &&
 79 |     memEx.computed === false;
 80 | }
 81 | 
 82 | function isAssignment(ex, node) {
 83 |   return ex.type === 'AssignmentExpression' &&
 84 |     ex.left === node;
 85 | }
 86 | 
 87 | function isUpdate(ex, node) {
 88 |   return ex.type === 'UpdateExpression' &&
 89 |     ex.argument === node;
 90 | }
 91 | 
 92 | function isMethodCall(ex, node) {
 93 |   return ex.type === 'CallExpression' &&
 94 |     ex.callee === node;
 95 | }
 96 | 
 97 | function variableExists(variableName, scope) {
 98 |   while (scope) {
 99 |     if (scope.through.some(ref => ref.identifier.name === variableName)) {
100 |       return true;
101 |     }
102 |     if (scope.set.get(variableName)) {
103 |       return true;
104 |     }
105 |     scope = scope.upper;
106 |   }
107 |   return false;
108 | }
109 | 
110 | function isKeyword(name) {
111 |   return parser.tokenize(name)[0].type === 'Keyword';
112 | }
113 | 
114 | function uniqPropNames(exs) {
115 |   return uniq(exs.map(({property}) => property.name));
116 | }
117 | 
118 | // By default recast indents the ObjectPattern AST node
119 | // See: https://github.com/benjamn/recast/issues/240
120 | //
121 | // To work around this, we're building the desired string by ourselves,
122 | // and parsing it with Recast and extracting the ObjectPatter node.
123 | // Feeding this back to Recast will preserve the formatting.
124 | function createDestructPattern(exs) {
125 |   const props = uniqPropNames(exs).join(', ');
126 |   const js = `function foo({${props}}) {};`;
127 |   const ast = parse(js, {parser});
128 |   return ast.program.body[0].params[0];
129 | }
130 | 


--------------------------------------------------------------------------------
/src/transform/exponent.js:
--------------------------------------------------------------------------------
 1 | import {matches} from 'f-matches';
 2 | import traverser from '../traverser';
 3 | 
 4 | const isMathPow = matches({
 5 |   type: 'CallExpression',
 6 |   callee: {
 7 |     type: 'MemberExpression',
 8 |     computed: false,
 9 |     object: {
10 |       type: 'Identifier',
11 |       name: 'Math'
12 |     },
13 |     property: {
14 |       type: 'Identifier',
15 |       name: 'pow'
16 |     }
17 |   },
18 |   arguments: (args) => args.length === 2
19 | });
20 | 
21 | export default function(ast) {
22 |   traverser.replace(ast, {
23 |     enter(node) {
24 |       if (isMathPow(node)) {
25 |         return {
26 |           type: 'BinaryExpression',
27 |           operator: '**',
28 |           left: node.arguments[0],
29 |           right: node.arguments[1],
30 |         };
31 |       }
32 |     }
33 |   });
34 | }
35 | 


--------------------------------------------------------------------------------
/src/transform/forEach/index.js:
--------------------------------------------------------------------------------
 1 | import {tail} from 'lodash/fp';
 2 | import traverser from '../../traverser';
 3 | import isEqualAst from '../../utils/isEqualAst';
 4 | import {isReference} from '../../utils/variableType';
 5 | import copyComments from '../../utils/copyComments';
 6 | import matchAliasedForLoop from '../../utils/matchAliasedForLoop';
 7 | import validateForLoop from './validateForLoop';
 8 | 
 9 | export default function(ast, logger) {
10 |   traverser.replace(ast, {
11 |     enter(node) {
12 |       const matches = matchAliasedForLoop(node);
13 | 
14 |       if (matches) {
15 |         const warning = validateForLoop(node, matches);
16 |         if (warning) {
17 |           logger.warn(...warning, 'for-each');
18 |           return;
19 |         }
20 | 
21 |         return withComments(node, createForEach(matches));
22 |       }
23 | 
24 |       if (node.type === 'ForStatement') {
25 |         logger.warn(node, 'Unable to transform for loop', 'for-each');
26 |       }
27 |     }
28 |   });
29 | }
30 | 
31 | function withComments(node, forEach) {
32 |   copyComments({from: node, to: forEach});
33 |   copyComments({from: node.body.body[0], to: forEach});
34 |   return forEach;
35 | }
36 | 
37 | function createForEach({body, item, index, array}) {
38 |   const newBody = removeFirstBodyElement(body);
39 |   const params = createForEachParams(newBody, item, index);
40 |   return {
41 |     type: 'ExpressionStatement',
42 |     expression: {
43 |       type: 'CallExpression',
44 |       callee: {
45 |         type: 'MemberExpression',
46 |         object: array,
47 |         property: {
48 |           type: 'Identifier',
49 |           name: 'forEach'
50 |         }
51 |       },
52 |       arguments: [{
53 |         type: 'ArrowFunctionExpression',
54 |         params,
55 |         body: newBody
56 |       }]
57 |     }
58 |   };
59 | }
60 | 
61 | function removeFirstBodyElement(body) {
62 |   return {
63 |     ...body,
64 |     body: tail(body.body),
65 |   };
66 | }
67 | 
68 | function createForEachParams(newBody, item, index) {
69 |   if (indexUsedInBody(newBody, index)) {
70 |     return [item, index];
71 |   }
72 |   return [item];
73 | }
74 | 
75 | function indexUsedInBody(newBody, index) {
76 |   return traverser.find(newBody, (node, parent) => {
77 |     return isEqualAst(node, index) && isReference(node, parent);
78 |   });
79 | }
80 | 


--------------------------------------------------------------------------------
/src/transform/forEach/validateForLoop.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../../traverser';
 2 | 
 3 | /**
 4 |  * Checks that for-loop can be transformed to Array.forEach()
 5 |  *
 6 |  * Returns a warning message in case we can't transform.
 7 |  *
 8 |  * @param  {Object} node The ForStatement
 9 |  * @param  {Object} body BlockStatement that's body of ForStatement
10 |  * @param  {String} indexKind
11 |  * @param  {String} itemKind
12 |  * @return {Array} Array of node and warnings message or undefined on success.
13 |  */
14 | export default function validateForLoop(node, {body, indexKind, itemKind}) {
15 |   let statement;
16 |   if ((statement = returnUsed(body))) {
17 |     return [statement, 'Return statement used in for-loop body'];
18 |   }
19 |   else if ((statement = breakWithLabelUsed(body))) {
20 |     return [statement, 'Break statement with label used in for-loop body'];
21 |   }
22 |   else if ((statement = continueWithLabelUsed(body))) {
23 |     return [statement, 'Continue statement with label used in for-loop body'];
24 |   }
25 |   else if ((statement = breakUsed(body))) {
26 |     return [statement, 'Break statement used in for-loop body'];
27 |   }
28 |   else if ((statement = continueUsed(body))) {
29 |     return [statement, 'Continue statement used in for-loop body'];
30 |   }
31 |   else if (indexKind !== 'let') {
32 |     return [node, 'Only for-loops with indexes declared as let can be transformed (use let transform first)'];
33 |   }
34 |   else if (itemKind !== 'const') {
35 |     return [node, 'Only for-loops with const array items can be transformed (use let transform first)'];
36 |   }
37 | }
38 | 
39 | const loopStatements = ['ForStatement', 'ForInStatement', 'ForOfStatement', 'DoWhileStatement', 'WhileStatement'];
40 | 
41 | function returnUsed(body) {
42 |   return traverser.find(body, 'ReturnStatement');
43 | }
44 | 
45 | function breakWithLabelUsed(body) {
46 |   return traverser.find(body, ({type, label}) => type === 'BreakStatement' && label);
47 | }
48 | 
49 | function continueWithLabelUsed(body) {
50 |   return traverser.find(body, ({type, label}) => type === 'ContinueStatement' && label);
51 | }
52 | 
53 | function breakUsed(body) {
54 |   return traverser.find(body, 'BreakStatement', {skipTypes: [...loopStatements, 'SwitchStatement']});
55 | }
56 | 
57 | function continueUsed(body) {
58 |   return traverser.find(body, 'ContinueStatement', {skipTypes: loopStatements});
59 | }
60 | 


--------------------------------------------------------------------------------
/src/transform/forOf.js:
--------------------------------------------------------------------------------
 1 | import {tail} from 'lodash/fp';
 2 | import traverser from '../traverser';
 3 | import isEqualAst from '../utils/isEqualAst';
 4 | import {isReference} from '../utils/variableType';
 5 | import copyComments from '../utils/copyComments';
 6 | import matchAliasedForLoop from '../utils/matchAliasedForLoop';
 7 | 
 8 | export default function(ast, logger) {
 9 |   traverser.replace(ast, {
10 |     enter(node) {
11 |       const matches = matchAliasedForLoop(node);
12 | 
13 |       if (matches) {
14 |         if (indexUsedInBody(matches)) {
15 |           logger.warn(node, 'Index variable used in for-loop body', 'for-of');
16 |           return;
17 |         }
18 | 
19 |         if (matches.itemKind === 'var' || matches.indexKind === 'var') {
20 |           logger.warn(node, 'Only for-loops with let/const can be transformed (use let transform first)', 'for-of');
21 |           return;
22 |         }
23 | 
24 |         return withComments(node, createForOf(matches));
25 |       }
26 | 
27 |       if (node.type === 'ForStatement') {
28 |         logger.warn(node, 'Unable to transform for loop', 'for-of');
29 |       }
30 |     }
31 |   });
32 | }
33 | 
34 | function indexUsedInBody({body, index}) {
35 |   return traverser.find(removeFirstBodyElement(body), (node, parent) => {
36 |     return isEqualAst(node, index) && isReference(node, parent);
37 |   });
38 | }
39 | 
40 | function withComments(node, forOf) {
41 |   copyComments({from: node, to: forOf});
42 |   copyComments({from: node.body.body[0], to: forOf});
43 |   return forOf;
44 | }
45 | 
46 | function createForOf({item, itemKind, array, body}) {
47 |   return {
48 |     type: 'ForOfStatement',
49 |     left: {
50 |       type: 'VariableDeclaration',
51 |       declarations: [
52 |         {
53 |           type: 'VariableDeclarator',
54 |           id: item,
55 |           init: null // eslint-disable-line no-null/no-null
56 |         }
57 |       ],
58 |       kind: itemKind
59 |     },
60 |     right: array,
61 |     body: removeFirstBodyElement(body)
62 |   };
63 | }
64 | 
65 | function removeFirstBodyElement(body) {
66 |   return {
67 |     ...body,
68 |     body: tail(body.body),
69 |   };
70 | }
71 | 


--------------------------------------------------------------------------------
/src/transform/includes/comparison.js:
--------------------------------------------------------------------------------
 1 | import {isMinusOne, isZero} from './matchesIndexOf';
 2 | 
 3 | /**
 4 |  * True when indexOf() comparison can be translated to includes()
 5 |  * @param {Object} matches
 6 |  * @return {Boolean}
 7 |  */
 8 | export function isIncludesComparison({operator, index}) {
 9 |   switch (operator) {
10 |   case '!==':
11 |   case '!=':
12 |   case '>':
13 |     return isMinusOne(index);
14 |   case '>=':
15 |     return isZero(index);
16 |   default:
17 |     return false;
18 |   }
19 | }
20 | 
21 | /**
22 |  * True when indexOf() comparison can be translated to !includes()
23 |  * @param {Object} matches
24 |  * @return {Boolean}
25 |  */
26 | export function isNotIncludesComparison({operator, index}) {
27 |   switch (operator) {
28 |   case '===':
29 |   case '==':
30 |     return isMinusOne(index);
31 |   case '<':
32 |     return isZero(index);
33 |   default:
34 |     return false;
35 |   }
36 | }
37 | 


--------------------------------------------------------------------------------
/src/transform/includes/index.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../../traverser';
 2 | import matchesIndexOf from './matchesIndexOf';
 3 | import {isIncludesComparison, isNotIncludesComparison} from './comparison';
 4 | 
 5 | export default function(ast) {
 6 |   traverser.replace(ast, {
 7 |     enter(node) {
 8 |       const matches = matchesIndexOf(node);
 9 |       if (matches && isIncludesComparison(matches)) {
10 |         return createIncludes(matches);
11 |       }
12 |       if (matches && isNotIncludesComparison(matches)) {
13 |         return createNot(createIncludes(matches));
14 |       }
15 |     }
16 |   });
17 | }
18 | 
19 | function createNot(argument) {
20 |   return {
21 |     type: 'UnaryExpression',
22 |     operator: '!',
23 |     prefix: true,
24 |     argument,
25 |   };
26 | }
27 | 
28 | function createIncludes({object, searchElement}) {
29 |   return {
30 |     type: 'CallExpression',
31 |     callee: {
32 |       type: 'MemberExpression',
33 |       computed: false,
34 |       object: object,
35 |       property: {
36 |         type: 'Identifier',
37 |         name: 'includes'
38 |       }
39 |     },
40 |     arguments: [searchElement]
41 |   };
42 | }
43 | 


--------------------------------------------------------------------------------
/src/transform/includes/matchesIndexOf.js:
--------------------------------------------------------------------------------
 1 | import {matches, matchesLength, extract, extractAny} from 'f-matches';
 2 | 
 3 | /**
 4 |  * Matches: -1
 5 |  */
 6 | export const isMinusOne = matches({
 7 |   type: 'UnaryExpression',
 8 |   operator: '-',
 9 |   argument: {
10 |     type: 'Literal',
11 |     value: 1
12 |   },
13 |   prefix: true
14 | });
15 | 
16 | /**
17 |  * Matches: 0
18 |  */
19 | export const isZero = matches({
20 |   type: 'Literal',
21 |   value: 0
22 | });
23 | 
24 | // Matches: object.indexOf(searchElement)
25 | const matchesCallIndexOf = matches({
26 |   type: 'CallExpression',
27 |   callee: {
28 |     type: 'MemberExpression',
29 |     computed: false,
30 |     object: extractAny('object'),
31 |     property: {
32 |       type: 'Identifier',
33 |       name: 'indexOf'
34 |     }
35 |   },
36 |   arguments: matchesLength([
37 |     extractAny('searchElement')
38 |   ])
39 | });
40 | 
41 | // Matches: -1 or 0
42 | const matchesIndex = extract('index', (v) => isMinusOne(v) || isZero(v));
43 | 
44 | // Matches: object.indexOf(searchElement) <operator> index
45 | const matchesIndexOfNormal = matches({
46 |   type: 'BinaryExpression',
47 |   operator: extractAny('operator'),
48 |   left: matchesCallIndexOf,
49 |   right: matchesIndex,
50 | });
51 | 
52 | // Matches: index <operator> object.indexOf(searchElement)
53 | const matchesIndexOfReversed = matches({
54 |   type: 'BinaryExpression',
55 |   operator: extractAny('operator'),
56 |   left: matchesIndex,
57 |   right: matchesCallIndexOf,
58 | });
59 | 
60 | // Reverses the direction of comparison operator
61 | function reverseOperator(operator) {
62 |   return operator.replace(/[><]/, (op) => op === '>' ? '<' : '>');
63 | }
64 | 
65 | function reverseOperatorField(match) {
66 |   if (!match) {
67 |     return false;
68 |   }
69 | 
70 |   return {
71 |     ...match,
72 |     operator: reverseOperator(match.operator),
73 |   };
74 | }
75 | 
76 | /**
77 |  * Matches:
78 |  *
79 |  *    object.indexOf(searchElement) <operator> index
80 |  *
81 |  * or
82 |  *
83 |  *    index <operator> object.indexOf(searchElement)
84 |  *
85 |  * On success returns object with keys:
86 |  *
87 |  * - object
88 |  * - searchElement
89 |  * - operator
90 |  * - index
91 |  *
92 |  * @param  {Object} node
93 |  * @return {Object}
94 |  */
95 | export default function(node) {
96 |   return matchesIndexOfNormal(node) || reverseOperatorField(matchesIndexOfReversed(node));
97 | }
98 | 


--------------------------------------------------------------------------------
/src/transform/multiVar.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../traverser';
 2 | import multiReplaceStatement from '../utils/multiReplaceStatement';
 3 | import VariableDeclaration from '../syntax/VariableDeclaration';
 4 | 
 5 | export default function(ast, logger) {
 6 |   traverser.traverse(ast, {
 7 |     enter(node, parent) {
 8 |       if (node.type === 'VariableDeclaration' && node.declarations.length > 1) {
 9 |         splitDeclaration(node, parent, logger);
10 | 
11 |         return traverser.VisitorOption.Skip;
12 |       }
13 |     }
14 |   });
15 | }
16 | 
17 | function splitDeclaration(node, parent, logger) {
18 |   const declNodes = node.declarations.map(declarator => {
19 |     return new VariableDeclaration(node.kind, [declarator]);
20 |   });
21 | 
22 |   try {
23 |     multiReplaceStatement({
24 |       parent,
25 |       node,
26 |       replacements: declNodes,
27 |       preserveComments: true,
28 |     });
29 |   }
30 |   catch (e) {
31 |     logger.warn(parent, `Unable to split var statement in a ${parent.type}`, 'multi-var');
32 |   }
33 | }
34 | 


--------------------------------------------------------------------------------
/src/transform/noStrict.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../traverser';
 2 | import isString from '../utils/isString';
 3 | import copyComments from '../utils/copyComments';
 4 | 
 5 | export default function(ast) {
 6 |   traverser.replace(ast, {
 7 |     enter(node, parent) {
 8 |       if (node.type === 'ExpressionStatement' && isUseStrictString(node.expression)) {
 9 |         copyComments({
10 |           from: node,
11 |           to: parent,
12 |         });
13 | 
14 |         this.remove();
15 |       }
16 |     }
17 |   });
18 | }
19 | 
20 | function isUseStrictString(node) {
21 |   return isString(node) && node.value === 'use strict';
22 | }
23 | 


--------------------------------------------------------------------------------
/src/transform/objMethod.js:
--------------------------------------------------------------------------------
 1 | import {matches, extractAny} from 'f-matches';
 2 | import traverser from '../traverser';
 3 | 
 4 | const matchTransformableProperty = matches({
 5 |   type: 'Property',
 6 |   key: {
 7 |     type: 'Identifier',
 8 |   },
 9 |   value: {
10 |     type: 'FunctionExpression',
11 |     id: extractAny('functionName'),
12 |   },
13 |   method: false,
14 |   computed: false,
15 |   shorthand: false
16 | });
17 | 
18 | export default function(ast, logger) {
19 |   traverser.replace(ast, {
20 |     enter(node) {
21 |       const match = matchTransformableProperty(node);
22 |       if (match) {
23 |         // Do not transform functions with name,
24 |         // as the name might be recursively referenced from inside.
25 |         if (match.functionName) {
26 |           logger.warn(node, 'Unable to transform named function', 'obj-method');
27 |           return;
28 |         }
29 | 
30 |         node.method = true;
31 |       }
32 |     }
33 |   });
34 | }
35 | 


--------------------------------------------------------------------------------
/src/transform/objShorthand.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../traverser';
 2 | 
 3 | export default function(ast) {
 4 |   traverser.replace(ast, {
 5 |     enter: propertyToShorthand
 6 |   });
 7 | }
 8 | 
 9 | function propertyToShorthand(node) {
10 |   if (node.type === 'Property' && equalIdentifiers(node.key, node.value)) {
11 |     node.shorthand = true;
12 |   }
13 | }
14 | 
15 | function equalIdentifiers(a, b) {
16 |   return a.type === 'Identifier' && b.type === 'Identifier' && a.name === b.name;
17 | }
18 | 


--------------------------------------------------------------------------------
/src/transform/template.js:
--------------------------------------------------------------------------------
  1 | import traverser from '../traverser';
  2 | import TemplateLiteral from './../syntax/TemplateLiteral';
  3 | import TemplateElement from './../syntax/TemplateElement';
  4 | import isString from './../utils/isString';
  5 | import {sortBy, flatten} from 'lodash/fp';
  6 | 
  7 | export default function(ast) {
  8 |   traverser.replace(ast, {
  9 |     enter(node) {
 10 |       if (isPlusExpression(node)) {
 11 |         const plusExpr = flattenPlusExpression(node);
 12 | 
 13 |         if (plusExpr.isString && !plusExpr.operands.every(isString)) {
 14 |           const literal = new TemplateLiteral(splitQuasisAndExpressions(plusExpr.operands));
 15 |           // Ensure correct order of comments by sorting them by their position in source
 16 |           literal.comments = sortBy('start', plusExpr.comments);
 17 |           return literal;
 18 |         }
 19 |       }
 20 |     }
 21 |   });
 22 | }
 23 | 
 24 | // Returns object of three fields:
 25 | // - operands: flat array of all the plus operation sub-expressions
 26 | // - comments: array of comments
 27 | // - isString: true when the result of the whole plus operation is a string
 28 | function flattenPlusExpression(node) {
 29 |   if (isPlusExpression(node)) {
 30 |     const left = flattenPlusExpression(node.left);
 31 |     const right = flattenPlusExpression(node.right);
 32 | 
 33 |     if (left.isString || right.isString) {
 34 |       return {
 35 |         operands: flatten([left.operands, right.operands]),
 36 |         comments: flatten([
 37 |           node.comments || [],
 38 |           left.comments,
 39 |           right.comments
 40 |         ]),
 41 |         isString: true,
 42 |       };
 43 |     }
 44 |     else {
 45 |       return {
 46 |         operands: [node],
 47 |         comments: node.comments || [],
 48 |         isString: false,
 49 |       };
 50 |     }
 51 |   }
 52 |   else {
 53 |     return {
 54 |       operands: [node],
 55 |       comments: node.comments || [],
 56 |       isString: isString(node),
 57 |     };
 58 |   }
 59 | }
 60 | 
 61 | function isPlusExpression(node) {
 62 |   return node.type === 'BinaryExpression' && node.operator === '+';
 63 | }
 64 | 
 65 | function splitQuasisAndExpressions(operands) {
 66 |   const quasis = [];
 67 |   const expressions = [];
 68 | 
 69 |   for (let i = 0; i < operands.length; i++) {
 70 |     const curr = operands[i];
 71 | 
 72 |     if (isString(curr)) {
 73 |       let currVal = curr.value;
 74 |       let currRaw = escapeForTemplate(curr.raw);
 75 | 
 76 |       while (isString(operands[i + 1] || {})) {
 77 |         i++;
 78 |         currVal += operands[i].value;
 79 |         currRaw += escapeForTemplate(operands[i].raw);
 80 |       }
 81 | 
 82 |       quasis.push(new TemplateElement({
 83 |         raw: currRaw,
 84 |         cooked: currVal
 85 |       }));
 86 |     }
 87 |     else {
 88 |       if (i === 0) {
 89 |         quasis.push(new TemplateElement({}));
 90 |       }
 91 | 
 92 |       if (!isString(operands[i + 1] || {})) {
 93 |         quasis.push(new TemplateElement({
 94 |           tail: operands[i + 1] === undefined
 95 |         }));
 96 |       }
 97 | 
 98 |       expressions.push(curr);
 99 |     }
100 |   }
101 | 
102 |   return {quasis, expressions};
103 | }
104 | 
105 | // Strip surrounding quotes, escape backticks and unescape escaped quotes
106 | function escapeForTemplate(raw) {
107 |   return raw
108 |     .replace(/^['"]|['"]$/g, '')
109 |     .replace(/`/g, '\\`')
110 |     .replace(/\\(['"])/g, '$1');
111 | }
112 | 


--------------------------------------------------------------------------------
/src/traverser.js:
--------------------------------------------------------------------------------
 1 | import {includes, isString} from 'lodash/fp';
 2 | import estraverse from 'estraverse';
 3 | 
 4 | // JSX AST types, as documented in:
 5 | // https://github.com/facebook/jsx/blob/master/AST.md
 6 | const jsxExtensionKeys = {
 7 |   keys: {
 8 |     JSXIdentifier: [],
 9 |     JSXMemberExpression: ['object', 'property'],
10 |     JSXNamespacedName: ['namespace', 'name'],
11 |     JSXEmptyExpression: [],
12 |     JSXExpressionContainer: ['expression'],
13 |     JSXOpeningElement: ['name', 'attributes'],
14 |     JSXClosingElement: ['name'],
15 |     JSXOpeningFragment: [],
16 |     JSXClosingFragment: [],
17 |     JSXAttribute: ['name', 'value'],
18 |     JSXSpreadAttribute: ['argument'],
19 |     JSXElement: ['openingElement', 'closingElement', 'children'],
20 |     JSXFragment: ['openingFragment', 'closingFragment', 'children'],
21 |     JSXText: [],
22 |   }
23 | };
24 | 
25 | /**
26 |  * Proxy for ESTraverse.
27 |  * Providing a single place to easily extend its functionality.
28 |  *
29 |  * Exposes the traverse() and replace() methods just like ESTraverse,
30 |  * plus some custom helpers.
31 |  */
32 | export default {
33 |   /**
34 |    * Traverses AST like ESTraverse.traverse()
35 |    * @param  {Object} tree
36 |    * @param  {Object} cfg Object with optional enter() and leave() methods.
37 |    * @return {Object} The transformed tree
38 |    */
39 |   traverse(tree, cfg) {
40 |     return estraverse.traverse(tree, Object.assign(cfg, jsxExtensionKeys));
41 |   },
42 | 
43 |   /**
44 |    * Traverses AST like ESTraverse.replace()
45 |    * @param  {Object} tree
46 |    * @param  {Object} cfg Object with optional enter() and leave() methods.
47 |    * @return {Object} The transformed tree
48 |    */
49 |   replace(tree, cfg) {
50 |     return estraverse.replace(tree, Object.assign(cfg, jsxExtensionKeys));
51 |   },
52 | 
53 |   /**
54 |    * Constants to return from enter()/leave() to control traversal:
55 |    *
56 |    * - Skip - skips walking child nodes
57 |    * - Break - ends it all
58 |    * - Remove - removes the current node (only with replace())
59 |    */
60 |   VisitorOption: estraverse.VisitorOption,
61 | 
62 |   /**
63 |    * Searches in AST tree for node which satisfies the predicate.
64 |    * @param  {Object} tree
65 |    * @param  {Function|String} query Search function called with `node` and `parent`
66 |    *   Alternatively it can be string: the node type to search for.
67 |    * @param  {String[]} opts.skipTypes List of node types to skip (not traversing into these nodes)
68 |    * @return {Object} The found node or undefined when not found
69 |    */
70 |   find(tree, query, {skipTypes = []} = {}) {
71 |     const predicate = this.createFindPredicate(query);
72 |     let found;
73 | 
74 |     this.traverse(tree, {
75 |       enter(node, parent) {
76 |         if (includes(node.type, skipTypes)) {
77 |           return estraverse.VisitorOption.Skip;
78 |         }
79 |         if (predicate(node, parent)) {
80 |           found = node;
81 |           return estraverse.VisitorOption.Break;
82 |         }
83 |       }
84 |     });
85 | 
86 |     return found;
87 |   },
88 | 
89 |   createFindPredicate(query) {
90 |     if (isString(query)) {
91 |       return (node) => node.type === query;
92 |     }
93 |     else {
94 |       return query;
95 |     }
96 |   }
97 | };
98 | 


--------------------------------------------------------------------------------
/src/utils/Hierarchy.js:
--------------------------------------------------------------------------------
 1 | import traverser from '../traverser';
 2 | 
 3 | /**
 4 |  * Provides a way to look up parent nodes.
 5 |  */
 6 | export default class Hierarchy {
 7 |   /**
 8 |    * @param {Object} ast Root node
 9 |    */
10 |   constructor(ast) {
11 |     this.parents = new Map();
12 | 
13 |     traverser.traverse(ast, {
14 |       enter: (node, parent) => {
15 |         this.parents.set(node, parent);
16 |       }
17 |     });
18 |   }
19 | 
20 |   /**
21 |    * Returns parent node of given AST node.
22 |    * @param {Object} node
23 |    * @return {Object}
24 |    */
25 |   getParent(node) {
26 |     return this.parents.get(node);
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/src/utils/copyComments.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Appends comments of one node to comments of another.
 3 |  *
 4 |  * - Modifies `to` node with added comments.
 5 |  * - Does nothing when there are no comments to copy
 6 |  *   (ensuring we don't modify the `to` node when not needed).
 7 |  *
 8 |  * @param  {Object} from Node to copy comments from
 9 |  * @param  {Object} to Node to copy comments to
10 |  */
11 | export default function copyComments({from, to}) {
12 |   if (from.comments && from.comments.length > 0) {
13 |     to.comments = (to.comments || []).concat(from.comments || []);
14 |   }
15 | }
16 | 


--------------------------------------------------------------------------------
/src/utils/destructuring.js:
--------------------------------------------------------------------------------
 1 | import {flatMap, compact} from 'lodash/fp';
 2 | 
 3 | /**
 4 |  * Extracts all variables from from destructuring
 5 |  * operation in assignment or variable declaration.
 6 |  *
 7 |  * Also works for a single identifier (so it generalizes
 8 |  * for all assignments / variable declarations).
 9 |  *
10 |  * @param  {Object} node
11 |  * @return {Object[]} Identifiers
12 |  */
13 | export function extractVariables(node) {
14 |   if (node.type === 'Identifier') {
15 |     return [node];
16 |   }
17 | 
18 |   if (node.type === 'ArrayPattern') {
19 |     // Use compact() to ignore missing elements in ArrayPattern
20 |     return flatMap(extractVariables, compact(node.elements));
21 |   }
22 |   if (node.type === 'ObjectPattern') {
23 |     return flatMap(extractVariables, node.properties);
24 |   }
25 |   if (node.type === 'Property') {
26 |     return extractVariables(node.value);
27 |   }
28 |   if (node.type === 'AssignmentPattern') {
29 |     return extractVariables(node.left);
30 |   }
31 | 
32 |   // Ignore stuff like MemberExpressions,
33 |   // we only care about variables.
34 |   return [];
35 | }
36 | 
37 | /**
38 |  * Like extractVariables(), but returns the names of variables
39 |  * instead of Identifier objects.
40 |  *
41 |  * @param  {Object} node
42 |  * @return {String[]} variable names
43 |  */
44 | export function extractVariableNames(node) {
45 |   return extractVariables(node).map(v => v.name);
46 | }
47 | 


--------------------------------------------------------------------------------
/src/utils/functionType.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * True when node is any kind of function.
 3 |  */
 4 | export function isFunction(node) {
 5 |   return isFunctionDeclaration(node) || isFunctionExpression(node);
 6 | }
 7 | 
 8 | /**
 9 |  * True when node is (arrow) function expression.
10 |  */
11 | export function isFunctionExpression(node) {
12 |   return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
13 | }
14 | 
15 | /**
16 |  * True when node is function declaration.
17 |  */
18 | export function isFunctionDeclaration(node) {
19 |   return node.type === 'FunctionDeclaration';
20 | }
21 | 


--------------------------------------------------------------------------------
/src/utils/isEqualAst.js:
--------------------------------------------------------------------------------
 1 | import {isEqualWith} from 'lodash/fp';
 2 | 
 3 | const metaDataFields = {
 4 |   comments: true,
 5 |   loc: true,
 6 |   start: true,
 7 |   end: true,
 8 | };
 9 | 
10 | /**
11 |  * True when two AST nodes are structurally equal.
12 |  * When comparing objects it ignores the meta-data fields for
13 |  * comments and source-code position.
14 |  * @param  {Object}  a
15 |  * @param  {Object}  b
16 |  * @return {Boolean}
17 |  */
18 | export default function isEqualAst(a, b) {
19 |   return isEqualWith((aValue, bValue, key) => metaDataFields[key], a, b);
20 | }
21 | 


--------------------------------------------------------------------------------
/src/utils/isString.js:
--------------------------------------------------------------------------------
1 | /**
2 |  * True when the given node is string literal.
3 |  * @param  {Object}  node
4 |  * @return {Boolean}
5 |  */
6 | export default function isString(node) {
7 |   return node.type === 'Literal' && typeof node.value === 'string';
8 | }
9 | 


--------------------------------------------------------------------------------
/src/utils/matchAliasedForLoop.js:
--------------------------------------------------------------------------------
  1 | import isEqualAst from './isEqualAst';
  2 | import {matches, extract, extractAny, matchesLength} from 'f-matches';
  3 | 
  4 | // Matches <ident>++ or ++<ident>
  5 | const matchPlusPlus = matches({
  6 |   type: 'UpdateExpression',
  7 |   operator: '++',
  8 |   argument: extract('indexIncrement', {
  9 |     type: 'Identifier',
 10 |   })
 11 | });
 12 | 
 13 | // Matches <ident>+=1
 14 | const matchPlusOne = matches({
 15 |   type: 'AssignmentExpression',
 16 |   operator: '+=',
 17 |   left: extract('indexIncrement', {
 18 |     type: 'Identifier',
 19 |   }),
 20 |   right: {
 21 |     type: 'Literal',
 22 |     value: 1
 23 |   }
 24 | });
 25 | 
 26 | // Matches the first element in array pattern
 27 | // The default matches() tries to match against any array element.
 28 | const matchesFirst = (patterns) => (array) => {
 29 |   return matches(patterns[0], array[0]);
 30 | };
 31 | 
 32 | // Matches for-loop
 33 | // without checking the consistency of index and array variables:
 34 | //
 35 | // for (let index = 0; indexComparison < array.length; indexIncrement++) {
 36 | //     let item = arrayReference[indexReference];
 37 | //     ...
 38 | // }
 39 | const matchLooseForLoop = matches({
 40 |   type: 'ForStatement',
 41 |   init: {
 42 |     type: 'VariableDeclaration',
 43 |     declarations: matchesLength([
 44 |       {
 45 |         type: 'VariableDeclarator',
 46 |         id: extract('index', {
 47 |           type: 'Identifier',
 48 |         }),
 49 |         init: {
 50 |           type: 'Literal',
 51 |           value: 0,
 52 |         }
 53 |       }
 54 |     ]),
 55 |     kind: extractAny('indexKind')
 56 |   },
 57 |   test: {
 58 |     type: 'BinaryExpression',
 59 |     operator: '<',
 60 |     left: extract('indexComparison', {
 61 |       type: 'Identifier',
 62 |     }),
 63 |     right: {
 64 |       type: 'MemberExpression',
 65 |       computed: false,
 66 |       object: extractAny('array'),
 67 |       property: {
 68 |         type: 'Identifier',
 69 |         name: 'length'
 70 |       }
 71 |     }
 72 |   },
 73 |   update: (node) => matchPlusPlus(node) || matchPlusOne(node),
 74 |   body: extract('body', {
 75 |     type: 'BlockStatement',
 76 |     body: matchesFirst([
 77 |       {
 78 |         type: 'VariableDeclaration',
 79 |         declarations: [
 80 |           {
 81 |             type: 'VariableDeclarator',
 82 |             id: extract('item', {
 83 |               type: 'Identifier',
 84 |             }),
 85 |             init: {
 86 |               type: 'MemberExpression',
 87 |               computed: true,
 88 |               object: extractAny('arrayReference'),
 89 |               property: extract('indexReference', {
 90 |                 type: 'Identifier',
 91 |               })
 92 |             }
 93 |           }
 94 |         ],
 95 |         kind: extractAny('itemKind')
 96 |       }
 97 |     ])
 98 |   })
 99 | });
100 | 
101 | function isConsistentIndexVar({index, indexComparison, indexIncrement, indexReference}) {
102 |   return isEqualAst(index, indexComparison) &&
103 |     isEqualAst(index, indexIncrement) &&
104 |     isEqualAst(index, indexReference);
105 | }
106 | 
107 | function isConsistentArrayVar({array, arrayReference}) {
108 |   return isEqualAst(array, arrayReference);
109 | }
110 | 
111 | /**
112 |  * Matches for-loop that aliases current array element
113 |  * in the first line of the loop body:
114 |  *
115 |  *     for (let index = 0; index < array.length; index++) {
116 |  *         let item = array[index];
117 |  *         ...
118 |  *     }
119 |  *
120 |  * Extracts the following fields:
121 |  *
122 |  * - index - loop index identifier
123 |  * - indexKind - the kind of <index>
124 |  * - array - array identifier or expression
125 |  * - item - identifier used to alias current array element
126 |  * - itemKind - the kind of <item>
127 |  * - body - the whole BlockStatement of for-loop body
128 |  *
129 |  * @param  {Object} node
130 |  * @return {Object}
131 |  */
132 | export default function(ast) {
133 |   const match = matchLooseForLoop(ast);
134 |   if (match && isConsistentIndexVar(match) && isConsistentArrayVar(match)) {
135 |     return match;
136 |   }
137 | }
138 | 


--------------------------------------------------------------------------------
/src/utils/multiReplaceStatement.js:
--------------------------------------------------------------------------------
 1 | import copyComments from './copyComments';
 2 | 
 3 | /**
 4 |  * Replaces `node` inside `parent` with any number of `replacements`.
 5 |  *
 6 |  * ESTraverse only allows replacing one node with a single other node.
 7 |  * This function overcomes this limitation, allowing to replace it with multiple nodes.
 8 |  *
 9 |  * NOTE: Only works for nodes that allow multiple elements in their body.
10 |  *       When node doesn't exist inside parent, does nothing.
11 |  *
12 |  * @param  {Object} cfg
13 |  *   @param  {Object} cfg.parent Parent node of the node to replace
14 |  *   @param  {Object} cfg.node The node to replace
15 |  *   @param  {Object[]} cfg.replacements Replacement nodes (can be empty array)
16 |  *   @param  {Boolean} [cfg.preserveComments] True to copy over comments from
17 |  *     node to first replacement node
18 |  */
19 | export default function multiReplaceStatement({parent, node, replacements, preserveComments}) {
20 |   const body = getBody(parent);
21 |   const index = body.indexOf(node);
22 |   if (preserveComments && replacements[0]) {
23 |     copyComments({from: node, to: replacements[0]});
24 |   }
25 |   if (index !== -1) {
26 |     body.splice(index, 1, ...replacements);
27 |   }
28 | }
29 | 
30 | function getBody(node) {
31 |   switch (node.type) {
32 |   case 'BlockStatement':
33 |   case 'Program':
34 |     return node.body;
35 |   case 'SwitchCase':
36 |     return node.consequent;
37 |   case 'ObjectExpression':
38 |     return node.properties;
39 |   default:
40 |     throw `Unsupported node type '${node.type}' in multiReplaceStatement()`;
41 |   }
42 | }
43 | 


--------------------------------------------------------------------------------
/src/utils/variableType.js:
--------------------------------------------------------------------------------
 1 | import {isFunction} from './functionType';
 2 | 
 3 | /**
 4 |  * True when node is variable update expression (like x++).
 5 |  *
 6 |  * @param {Object} node
 7 |  * @return {Boolean}
 8 |  */
 9 | export function isUpdate(node) {
10 |   return node.type === 'UpdateExpression' && node.argument.type === 'Identifier';
11 | }
12 | 
13 | /**
14 |  * True when node is reference to a variable.
15 |  *
16 |  * That is it's an identifier, that's not used:
17 |  *
18 |  * - as function name in function declaration/expression,
19 |  * - as parameter name in function declaration/expression,
20 |  * - as declared variable name in variable declaration,
21 |  * - as object property name in member expression.
22 |  * - as object property name in object literal.
23 |  *
24 |  * @param {Object} node
25 |  * @param {Object} parent Immediate parent node (to determine context)
26 |  * @return {Boolean}
27 |  */
28 | export function isReference(node, parent) {
29 |   return node.type === 'Identifier' &&
30 |     !isFunctionName(node, parent) &&
31 |     !isFunctionParameter(node, parent) &&
32 |     !isDeclaredVariable(node, parent) &&
33 |     !isPropertyInMemberExpression(node, parent) &&
34 |     !isPropertyInObjectLiteral(node, parent);
35 | }
36 | 
37 | function isFunctionName(node, parent) {
38 |   return isFunction(parent) && parent.id === node;
39 | }
40 | 
41 | function isFunctionParameter(node, parent) {
42 |   return isFunction(parent) && parent.params.some(p => p === node);
43 | }
44 | 
45 | function isDeclaredVariable(node, parent) {
46 |   return parent.type === 'VariableDeclarator' && parent.id === node;
47 | }
48 | 
49 | function isPropertyInMemberExpression(node, parent) {
50 |   return parent.type === 'MemberExpression' && parent.property === node && !parent.computed;
51 | }
52 | 
53 | function isPropertyInObjectLiteral(node, parent) {
54 |   return parent.type === 'Property' && parent.key === node;
55 | }
56 | 


--------------------------------------------------------------------------------
/src/withScope.js:
--------------------------------------------------------------------------------
 1 | import {analyze} from 'escope';
 2 | import {isFunction} from './utils/functionType';
 3 | const emptyFn = () => {}; // eslint-disable-line no-empty-function
 4 | 
 5 | /**
 6 |  * A helper for traversing with scope info from escope.
 7 |  *
 8 |  * Usage:
 9 |  *
10 |  *     traverser.traverse(ast, withScope(ast, {
11 |  *       enter(node, parent, scope) {
12 |  *         // do something with node and scope
13 |  *       }
14 |  *     }))
15 |  *
16 |  * @param {Object} ast The AST root node also passed to traverser.
17 |  * @param {Object} cfg Object with enter function as expected by traverser.
18 |  * @return {Object} Object with enter function to be passed to traverser.
19 |  */
20 | export default function withScope(ast, {enter = emptyFn}) {
21 |   const scopeManager = analyze(ast, {ecmaVersion: 2022, sourceType: 'module'});
22 |   let currentScope = scopeManager.acquire(ast);
23 | 
24 |   return {
25 |     enter(node, parent) {
26 |       if (isFunction(node)) {
27 |         currentScope = scopeManager.acquire(node);
28 |       }
29 |       return enter.call(this, node, parent, currentScope);
30 |     }
31 |     // NOTE: leave() is currently not implemented.
32 |     // See escope docs for supporting it if need arises: https://github.com/estools/escope
33 |   };
34 | }
35 | 


--------------------------------------------------------------------------------
/system-test/binTest.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable prefer-arrow-callback */
  2 | import {expect} from 'chai';
  3 | import fs from 'fs';
  4 | import {exec} from 'child_process';
  5 | 
  6 | const INPUT_FILE = 'system-test/test-data.js';
  7 | const INPUT_WARNINGS_FILE = 'system-test/test-data-warnings.js';
  8 | const OUTPUT_FILE = 'system-test/output.js';
  9 | 
 10 | describe('Smoke test for the executable script', function() {
 11 |   beforeEach(() => {
 12 |     fs.writeFileSync(
 13 |       INPUT_FILE,
 14 |       'var foo = 10;\n' +
 15 |       '[1, 2, 3].map(function(x) { return x*x });'
 16 |     );
 17 |   });
 18 | 
 19 |   afterEach(() => {
 20 |     fs.unlinkSync(INPUT_FILE);
 21 |     if (fs.existsSync(OUTPUT_FILE)) {
 22 |       fs.unlinkSync(OUTPUT_FILE);
 23 |     }
 24 |   });
 25 | 
 26 |   describe('when valid input and output file given', function() {
 27 |     it('transforms input file to output file', done => {
 28 |       exec(`node ./bin/index.js -t let,arrow ${INPUT_FILE} -o ${OUTPUT_FILE}`, (error, stdout, stderr) => {
 29 |         expect(error).to.equal(null); // eslint-disable-line no-null/no-null
 30 |         expect(stderr).to.equal('');
 31 |         expect(stdout).to.equal('');
 32 | 
 33 |         expect(fs.readFileSync(OUTPUT_FILE).toString()).to.equal(
 34 |           'const foo = 10;\n' +
 35 |           '[1, 2, 3].map(x => { return x*x });'
 36 |         );
 37 |         done();
 38 |       });
 39 |     });
 40 |   });
 41 | 
 42 |   describe('when no input/output files given', () => {
 43 |     it('reads STDIN and writes STDOUT', done => {
 44 |       exec(`node ./bin/index.js -t let,arrow < ${INPUT_FILE} > ${OUTPUT_FILE}`, (error, stdout, stderr) => {
 45 |         expect(error).to.equal(null); // eslint-disable-line no-null/no-null
 46 |         expect(stderr).to.equal('');
 47 |         expect(stdout).to.equal('');
 48 | 
 49 |         expect(fs.readFileSync(OUTPUT_FILE).toString()).to.equal(
 50 |           'const foo = 10;\n' +
 51 |           '[1, 2, 3].map(x => { return x*x });'
 52 |         );
 53 |         done();
 54 |       });
 55 |     });
 56 |   });
 57 | 
 58 |   describe('when invalid transform name given', () => {
 59 |     it('exits with error message', done => {
 60 |       exec(`node ./bin/index.js --transform blah ${INPUT_FILE}`, (error, stdout, stderr) => {
 61 |         expect(error).not.to.equal(null); // eslint-disable-line no-null/no-null
 62 |         expect(stderr).to.equal('Unknown transform "blah".\n');
 63 |         expect(stdout).to.equal('');
 64 | 
 65 |         expect(fs.existsSync(OUTPUT_FILE)).to.equal(false);
 66 |         done();
 67 |       });
 68 |     });
 69 |   });
 70 | 
 71 |   describe('when transform generates warnings', () => {
 72 |     beforeEach(() => {
 73 |       fs.writeFileSync(
 74 |         INPUT_WARNINGS_FILE,
 75 |         'if (true) { var x = 10; }\n x = 12;\n'
 76 |       );
 77 |     });
 78 | 
 79 |     afterEach(() => {
 80 |       fs.unlinkSync(INPUT_WARNINGS_FILE);
 81 |     });
 82 | 
 83 |     it('logs warnings to STDERR', done => {
 84 |       exec(`node ./bin/index.js --transform let ${INPUT_WARNINGS_FILE}`, (error, stdout, stderr) => {
 85 |         expect(error).to.equal(null); // eslint-disable-line no-null/no-null
 86 |         expect(stderr).to.equal(`${INPUT_WARNINGS_FILE}:\n1:  warning  Unable to transform var  (let)\n`);
 87 |         expect(stdout).to.equal('if (true) { var x = 10; }\n x = 12;\n');
 88 |         done();
 89 |       });
 90 |     });
 91 |   });
 92 | 
 93 |   describe('when parsing of file fails', () => {
 94 |     beforeEach(() => {
 95 |       fs.writeFileSync(
 96 |         INPUT_WARNINGS_FILE,
 97 |         'if (true) { @unknown!syntax; }\n'
 98 |       );
 99 |     });
100 | 
101 |     afterEach(() => {
102 |       fs.unlinkSync(INPUT_WARNINGS_FILE);
103 |     });
104 | 
105 |     it('writes error to STDERR', done => {
106 |       exec(`node ./bin/index.js --transform let ${INPUT_WARNINGS_FILE}`, (error, stdout, stderr) => {
107 |         expect(error.code).to.equal(1); // eslint-disable-line no-null/no-null
108 |         expect(stderr).to.contain(`Error transforming: ${INPUT_WARNINGS_FILE}\n`);
109 |         expect(stdout).to.equal('');
110 |         done();
111 |       });
112 |     });
113 |   });
114 | });
115 | 


--------------------------------------------------------------------------------
/system-test/commonjsApiTest.js:
--------------------------------------------------------------------------------
1 | import {testTransformApi} from './testTransformApi';
2 | const lebab = require('../index.js');
3 | 
4 | describe('ES5 commonjs API', () => {
5 |   testTransformApi(lebab.transform);
6 | });
7 | 


--------------------------------------------------------------------------------
/system-test/importApiTest.js:
--------------------------------------------------------------------------------
 1 | import {testTransformApi} from './testTransformApi';
 2 | import lebab, {transform} from '../index.js';
 3 | 
 4 | describe('ES6 default import API', () => {
 5 |   testTransformApi(lebab.transform);
 6 | });
 7 | 
 8 | describe('ES6 named import API', () => {
 9 |   testTransformApi(transform);
10 | });
11 | 


--------------------------------------------------------------------------------
/system-test/testTransformApi.js:
--------------------------------------------------------------------------------
 1 | import {expect} from 'chai';
 2 | 
 3 | export function testTransformApi(transform) {
 4 |   it('performs successful transform', () => {
 5 |     const {code, warnings} = transform(
 6 |       'var f = function(a) { return a; };',
 7 |       ['let', 'arrow', 'arrow-return']
 8 |     );
 9 |     expect(code).to.equal('const f = a => a;');
10 |     expect(warnings).to.deep.equal([]);
11 |   });
12 | 
13 |   it('outputs warnings', () => {
14 |     const input = 'if (true) { var x = 10; }\n x = 12;\n';
15 | 
16 |     const {code, warnings} = transform(input, ['let']);
17 |     expect(code).to.equal(input);
18 |     expect(warnings).to.deep.equal([
19 |       {line: 1, msg: 'Unable to transform var', type: 'let'},
20 |     ]);
21 |   });
22 | 
23 |   it('throws for invalid transform type', () => {
24 |     expect(() => {
25 |       transform('', ['blah']);
26 |     }).to.throw('Unknown transform "blah".');
27 |   });
28 | 
29 |   it('throws for syntax error in input code', () => {
30 |     expect(() => {
31 |       transform('@!class', ['let']);
32 |     }).to.throw('Unexpected character \'@\'');
33 |   });
34 | }
35 | 


--------------------------------------------------------------------------------
/test/OptionParserTest.js:
--------------------------------------------------------------------------------
 1 | import {expect} from 'chai';
 2 | import OptionParser from './../src/OptionParser';
 3 | 
 4 | function parse(argv) {
 5 |   return new OptionParser().parse(['node', 'script.js'].concat(argv));
 6 | }
 7 | 
 8 | describe('Command Line Interface', () => {
 9 |   it('when no transforms given, throws error', () => {
10 |     expect(() => {
11 |       parse([]);
12 |     }).to.throw('No transforms specified :(');
13 |   });
14 | 
15 |   it('when single transforms given, enables it', () => {
16 |     const options = parse(['-t', 'class']);
17 |     expect(options.transforms).to.deep.equal([
18 |       'class',
19 |     ]);
20 |   });
21 | 
22 |   it('when --transform=let,no-strict,commonjs given, enables only these transforms', () => {
23 |     const options = parse(['--transform', 'let,no-strict,commonjs']);
24 |     expect(options.transforms).to.deep.equal([
25 |       'let',
26 |       'no-strict',
27 |       'commonjs',
28 |     ]);
29 |   });
30 | 
31 |   it('accepts any transform name (transform names are validated later)', () => {
32 |     const options = parse(['--transform', 'unknown']);
33 |     expect(options.transforms).to.deep.equal(['unknown']);
34 |   });
35 | 
36 |   it('by default reads STDIN and writes to STDOUT', () => {
37 |     const options = parse(['-t', 'class']);
38 |     expect(options.inFile).to.equal(undefined);
39 |     expect(options.outFile).to.equal(undefined);
40 |     expect(options.replace).to.equal(undefined);
41 |   });
42 | 
43 |   it('when existing <filename> given reads <filename> and writes to STDOUT', () => {
44 |     const options = parse(['-t', 'class', 'lib/io.js']);
45 |     expect(options.inFile).to.equal('lib/io.js');
46 |     expect(options.outFile).to.equal(undefined);
47 |     expect(options.replace).to.equal(undefined);
48 |   });
49 | 
50 |   it('when not-existing <filename> given raises error', () => {
51 |     expect(() => {
52 |       parse(['-t', 'class', 'missing.js']);
53 |     }).to.throw('File missing.js does not exist.');
54 |   });
55 | 
56 |   it('when more than one <filename> given raises error', () => {
57 |     expect(() => {
58 |       parse(['-t', 'class', 'lib/io.js', 'lib/transformer.js']);
59 |     }).to.throw('Only one input file allowed, but 2 given instead.');
60 |   });
61 | 
62 |   it('when --out-file <filename> given writes <filename> and reads STDIN', () => {
63 |     const options = parse(['-t', 'class', '--out-file', 'some/file.js']);
64 |     expect(options.inFile).to.equal(undefined);
65 |     expect(options.outFile).to.equal('some/file.js');
66 |     expect(options.replace).to.equal(undefined);
67 |   });
68 | 
69 |   it('when --replace <dirname> given transforms all files in glob pattern', () => {
70 |     const options = parse(['-t', 'class', '--replace', '*.js']);
71 |     expect(options.inFile).to.equal(undefined);
72 |     expect(options.outFile).to.equal(undefined);
73 |     expect(options.replace).to.equal('*.js');
74 |   });
75 | 
76 |   it('when --replace used together with input file, raises error', () => {
77 |     expect(() => {
78 |       parse(['-t', 'class', '--replace', 'lib/', 'lib/io.js']);
79 |     }).to.throw('The --replace and plain input file options cannot be used together.');
80 |   });
81 | 
82 |   it('when --replace used together with output file, raises error', () => {
83 |     expect(() => {
84 |       parse(['-t', 'class', '--replace', 'lib/', '-o', 'some/file.js']);
85 |     }).to.throw('The --replace and --out-file options cannot be used together.');
86 |   });
87 | 
88 |   it('when --replace used with existing dir name, turns it into glob pattern', () => {
89 |     const options = parse(['-t', 'class', '--replace', 'lib/']);
90 |     expect(options.inFile).to.equal(undefined);
91 |     expect(options.outFile).to.equal(undefined);
92 |     expect(options.replace).to.be.oneOf(['lib/**/*.js', 'lib\\**\\*.js']);
93 |   });
94 | });
95 | 


--------------------------------------------------------------------------------
/test/createTestHelpers.js:
--------------------------------------------------------------------------------
 1 | import {expect} from 'chai';
 2 | import createTransformer from './../src/createTransformer';
 3 | 
 4 | /**
 5 |  * Generates functions that are used in all transform-tests.
 6 |  * @param  {String[]} transformNames Config for Transformer class
 7 |  * @return {Object} functions expectTransform() and expectNoChange()
 8 |  */
 9 | export default function(transformNames) {
10 |   const transformer = createTransformer(transformNames);
11 | 
12 |   // Generic transformation asserter, to be called like:
13 |   //
14 |   //   expectTransform("code")
15 |   //     .toReturn("transformed code");
16 |   //     .withWarnings(["My warning"]);
17 |   //
18 |   function expectTransform(script) {
19 |     const {code, warnings} = transformer.run(script);
20 | 
21 |     return {
22 |       toReturn(expectedValue) {
23 |         expect(code).to.equal(expectedValue);
24 |         return this;
25 |       },
26 |       withWarnings(expectedWarnings) {
27 |         expect(warnings).to.deep.equal(expectedWarnings);
28 |         return this;
29 |       },
30 |       withoutWarnings() {
31 |         return this.withWarnings([]);
32 |       },
33 |     };
34 |   }
35 | 
36 |   // Asserts that transforming the string has no effect,
37 |   // and also allows to check for warnings like so:
38 |   //
39 |   //   expectNoChange("code")
40 |   //     .withWarnings(["My warning"]);
41 |   //
42 |   function expectNoChange(script) {
43 |     return expectTransform(script).toReturn(script);
44 |   }
45 | 
46 |   return {expectTransform, expectNoChange};
47 | }
48 | 


--------------------------------------------------------------------------------
/test/transform/argRestTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['arg-rest']);
  3 | 
  4 | describe('Arguments variable to ...args', () => {
  5 |   it('does not replace arguments outside a function', () => {
  6 |     expectNoChange(
  7 |       'console.log(arguments);'
  8 |     );
  9 |   });
 10 | 
 11 |   it('replaces arguments in function declaration', () => {
 12 |     expectTransform(
 13 |       'function foo() {\n' +
 14 |       '  console.log(arguments);\n' +
 15 |       '}'
 16 |     ).toReturn(
 17 |       'function foo(...args) {\n' +
 18 |       '  console.log(args);\n' +
 19 |       '}'
 20 |     );
 21 |   });
 22 | 
 23 |   it('replaces arguments in function expression', () => {
 24 |     expectTransform(
 25 |       'var foo = function() {\n' +
 26 |       '  console.log(arguments);\n' +
 27 |       '};'
 28 |     ).toReturn(
 29 |       'var foo = function(...args) {\n' +
 30 |       '  console.log(args);\n' +
 31 |       '};'
 32 |     );
 33 |   });
 34 | 
 35 |   it('replaces arguments in class method', () => {
 36 |     expectTransform(
 37 |       'class Foo {\n' +
 38 |       '  bar() {\n' +
 39 |       '    console.log(arguments);\n' +
 40 |       '  }\n' +
 41 |       '}'
 42 |     ).toReturn(
 43 |       'class Foo {\n' +
 44 |       '  bar(...args) {\n' +
 45 |       '    console.log(args);\n' +
 46 |       '  }\n' +
 47 |       '}'
 48 |     );
 49 |   });
 50 | 
 51 |   // `arguments` in arrow function reference the `arguments` in enclosing scope
 52 | 
 53 |   it('does not replace arguments in arrow function', () => {
 54 |     expectNoChange(
 55 |       'var foo = () => console.log(arguments);'
 56 |     );
 57 |   });
 58 | 
 59 |   it('replaces arguments in nested arrow function', () => {
 60 |     expectTransform(
 61 |       'function foo() {\n' +
 62 |       '  var bar = () => console.log(arguments);\n' +
 63 |       '}'
 64 |     ).toReturn(
 65 |       'function foo(...args) {\n' +
 66 |       '  var bar = () => console.log(args);\n' +
 67 |       '}'
 68 |     );
 69 |   });
 70 | 
 71 |   // Handling of conflicts with existing variables
 72 |   it('does not replace arguments when args variable already exists', () => {
 73 |     expectNoChange(
 74 |       'function foo() {\n' +
 75 |       '  var args = [];\n' +
 76 |       '  console.log(arguments);\n' +
 77 |       '}'
 78 |     );
 79 |   });
 80 | 
 81 |   it('does not replace arguments when args variable exists in parent scope', () => {
 82 |     expectNoChange(
 83 |       'var args = [];\n' +
 84 |       'function foo() {\n' +
 85 |       '  console.log(args, arguments);\n' +
 86 |       '}'
 87 |     );
 88 |   });
 89 | 
 90 |   it('does not replace arguments when args variable exists in parent function param', () => {
 91 |     expectNoChange(
 92 |       'function parent(args) {\n' +
 93 |       '  function foo() {\n' +
 94 |       '    console.log(args, arguments);\n' +
 95 |       '  }\n' +
 96 |       '}'
 97 |     );
 98 |   });
 99 | 
100 |   it('does not replace arguments when args variable exists in child block scope that uses arguments', () => {
101 |     expectNoChange(
102 |       'function foo() {\n' +
103 |       '  if (true) {\n' +
104 |       '    const args = 0;\n' +
105 |       '    console.log(arguments);\n' +
106 |       '  }\n' +
107 |       '}'
108 |     );
109 |   });
110 | 
111 |   it('does not replace arguments in function declaration with existing formal params', () => {
112 |     expectNoChange(
113 |       'function foo(a, b ,c) {\n' +
114 |       '  console.log(arguments);\n' +
115 |       '}'
116 |     );
117 |   });
118 | 
119 |   it('does not add ...args to function that does not use arguments', () => {
120 |     expectNoChange(
121 |       'function foo() {\n' +
122 |       '  console.log(a, b, c);\n' +
123 |       '}'
124 |     );
125 |   });
126 | });
127 | 


--------------------------------------------------------------------------------
/test/transform/argSpreadTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectTransform, expectNoChange} = createTestHelpers(['arg-spread']);
 3 | 
 4 | describe('Arguments apply() to spread', () => {
 5 |   it('should convert basic obj.fn.apply()', () => {
 6 |     expectTransform(
 7 |       'obj.fn.apply(obj, someArray);'
 8 |     ).toReturn(
 9 |       'obj.fn(...someArray);'
10 |     );
11 |   });
12 | 
13 |   it('should convert this.method.apply()', () => {
14 |     expectTransform(
15 |       'this.method.apply(this, someArray);'
16 |     ).toReturn(
17 |       'this.method(...someArray);'
18 |     );
19 |   });
20 | 
21 |   it('should not convert obj.fn.apply() without obj as parameter', () => {
22 |     expectNoChange('obj.fn.apply(otherObj, someArray);');
23 |     expectNoChange('obj.fn.apply(undefined, someArray);');
24 |     expectNoChange('obj.fn.apply(null, someArray);');
25 |     expectNoChange('obj.fn.apply(this, someArray);');
26 |     expectNoChange('obj.fn.apply({}, someArray);');
27 |   });
28 | 
29 |   it('should convert plain fn.apply()', () => {
30 |     expectTransform('fn.apply(undefined, someArray);').toReturn('fn(...someArray);');
31 |     expectTransform('fn.apply(null, someArray);').toReturn('fn(...someArray);');
32 |   });
33 | 
34 |   it('should not convert plain fn.apply() when actual object used as this parameter', () => {
35 |     expectNoChange('fn.apply(obj, someArray);');
36 |     expectNoChange('fn.apply(this, someArray);');
37 |     expectNoChange('fn.apply({}, someArray);');
38 |   });
39 | 
40 |   it('should convert obj.fn.apply() with array expression', () => {
41 |     expectTransform(
42 |       'obj.fn.apply(obj, [1, 2, 3]);'
43 |     ).toReturn(
44 |       'obj.fn(...[1, 2, 3]);'
45 |     );
46 |   });
47 | 
48 |   it('should convert <long-expression>.fn.apply()', () => {
49 |     expectTransform(
50 |       'foo[bar+1].baz.fn.apply(foo[bar+1].baz, someArray);'
51 |     ).toReturn(
52 |       'foo[bar+1].baz.fn(...someArray);'
53 |     );
54 |   });
55 | 
56 |   it('should convert <literal>.fn.apply()', () => {
57 |     expectTransform(
58 |       '[].fn.apply([], someArray);'
59 |     ).toReturn(
60 |       '[].fn(...someArray);'
61 |     );
62 |   });
63 | 
64 |   it('should convert obj[fn].apply()', () => {
65 |     expectTransform(
66 |       'obj[fn].apply(obj, someArray);'
67 |     ).toReturn(
68 |       'obj[fn](...someArray);'
69 |     );
70 |   });
71 | });
72 | 


--------------------------------------------------------------------------------
/test/transform/arrowReturnTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['arrow-return']);
  3 | 
  4 | describe('Arrow functions with return', () => {
  5 |   it('should convert basic arrow function', () => {
  6 |     expectTransform(
  7 |       'a(() => { return 123; });'
  8 |     ).toReturn(
  9 |       'a(() => 123);'
 10 |     );
 11 |   });
 12 | 
 13 |   it('should convert arrow function inside variable declaration', () => {
 14 |     expectTransform(
 15 |       'const a = () => { return 123; }'
 16 |     ).toReturn(
 17 |       'const a = () => 123'
 18 |     );
 19 |   });
 20 | 
 21 |   it('should convert arrow function inside object', () => {
 22 |     expectTransform(
 23 |       '({ foo: () => { return 123; } })'
 24 |     ).toReturn(
 25 |       '({ foo: () => 123 })'
 26 |     );
 27 |   });
 28 | 
 29 |   it('should convert nested arrow functions', () => {
 30 |     expectTransform(
 31 |       'a(() => { return () => { const b = c => { return c; } }; })'
 32 |     ).toReturn(
 33 |       'a(() => () => { const b = c => c })'
 34 |     );
 35 |   });
 36 | 
 37 |   // Even when `this` or `arguments` is used inside arrow function,
 38 |   // it's still fine to convert it to shorthand syntax.
 39 |   // (We need to watch out for these in `arrow` transform though.)
 40 | 
 41 |   it('should convert arrow function using `this` keyword', () => {
 42 |     expectTransform(
 43 |       'function Foo() {\n' +
 44 |       '  setTimeout(() => { return this; });\n' +
 45 |       '}'
 46 |     ).toReturn(
 47 |       'function Foo() {\n' +
 48 |       '  setTimeout(() => this);\n' +
 49 |       '}'
 50 |     );
 51 |   });
 52 | 
 53 |   it('should convert arrow function using `arguments` keyword', () => {
 54 |     expectTransform(
 55 |       'function func() {\n' +
 56 |       '  setTimeout(() => { return arguments; });\n' +
 57 |       '}'
 58 |     ).toReturn(
 59 |       'function func() {\n' +
 60 |       '  setTimeout(() => arguments);\n' +
 61 |       '}'
 62 |     );
 63 |   });
 64 | 
 65 |   it('should convert returning an object', () => {
 66 |     expectTransform(
 67 |       'var f = a => { return {a: 1}; };'
 68 |     ).toReturn(
 69 |       'var f = a => ({\n' +
 70 |       '  a: 1\n' +
 71 |       '});'
 72 |     );
 73 |   });
 74 | 
 75 |   it('should convert returning an object property access', () => {
 76 |     expectTransform(
 77 |       'var f = (a) => { return {a: 1}[a]; };'
 78 |     ).toReturn(
 79 |       'var f = a => ({\n' +
 80 |       '  a: 1\n' +
 81 |       '})[a];'
 82 |     );
 83 |   });
 84 | 
 85 |   it('should convert return statements inside a parenthesized arrow function', () => {
 86 |     expectTransform(
 87 |       'const x = (a => { return a; }).call(null, 1);'
 88 |     ).toReturn(
 89 |       'const x = (a => a).call(null, 1);'
 90 |     );
 91 |   });
 92 | 
 93 |   it('should preserve comments', () => {
 94 |     expectTransform(
 95 |       'a(b => {\n' +
 96 |       '  // comment\n' +
 97 |       '  return b;\n' +
 98 |       '});'
 99 |     ).toReturn(
100 |       'a(b => // comment\n' +
101 |       'b);'
102 |     );
103 |   });
104 | 
105 |   it('should not convert arrow functions without return keyword', () => {
106 |     expectNoChange('a(() => {});');
107 |   });
108 | 
109 |   it('should not convert empty return', () => {
110 |     expectNoChange(
111 |       'var f = () => { return; };'
112 |     );
113 |   });
114 | 
115 |   it('should not convert return statements from non-arrow function', () => {
116 |     expectNoChange('const a = function(b) { return b; };');
117 |   });
118 | 
119 |   it('should not convert return statements from non-arrow function inside a nested arrow function', () => {
120 |     expectNoChange('const a = b => { const c = function(d) { return d; } };');
121 |   });
122 | 
123 |   it('should preserve code after return statement', () => {
124 |     expectNoChange(
125 |       'a(() => {\n' +
126 |       '  return func;\n' +
127 |       '  function func() {}\n' +
128 |       '});'
129 |     );
130 |   });
131 | });
132 | 


--------------------------------------------------------------------------------
/test/transform/commonjs/exportCommonjsTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['commonjs']);
  3 | 
  4 | describe('Export CommonJS', () => {
  5 |   describe('default export', () => {
  6 |     it('should convert module.exports assignment to default export', () => {
  7 |       expectTransform('module.exports = 123;').toReturn('export default 123;');
  8 |       expectTransform('module.exports = function() {};').toReturn('export default function() {};');
  9 |       expectTransform('module.exports = x => x;').toReturn('export default x => x;');
 10 |       expectTransform('module.exports = class {};').toReturn('export default class {};');
 11 |     });
 12 | 
 13 |     it('should not convert assignment to exports', () => {
 14 |       expectNoChange('exports = function() {};');
 15 |     });
 16 | 
 17 |     it('should not convert weird assignment to module.exports', () => {
 18 |       expectNoChange('module.exports += function() {};');
 19 |       expectNoChange('module.exports -= function() {};');
 20 |       expectNoChange('module.exports *= function() {};');
 21 |       expectNoChange('module.exports /= function() {};');
 22 |     });
 23 | 
 24 |     // A pretty weird thing to do...
 25 |     // shouldn't bother supporting it.
 26 |     it('should not convert assignment to module["exports"]', () => {
 27 |       expectNoChange('module["exports"] = function() {};');
 28 |     });
 29 | 
 30 |     it('should ignore module.exports inside statements', () => {
 31 |       expectNoChange(
 32 |         'if (true) {\n' +
 33 |         '  module.exports = function() {};\n' +
 34 |         '}'
 35 |       ).withWarnings([
 36 |         {line: 2, msg: 'export can only be at root level', type: 'commonjs'}
 37 |       ]);
 38 |     });
 39 |   });
 40 | 
 41 |   describe('named export', () => {
 42 |     it('should convert module.exports.foo = function () {}', () => {
 43 |       expectTransform('module.exports.foo = function () {};').toReturn('export function foo() {}');
 44 |     });
 45 | 
 46 |     it('should convert exports.foo = function () {}', () => {
 47 |       expectTransform('exports.foo = function () {};').toReturn('export function foo() {}');
 48 |     });
 49 | 
 50 |     it('should convert exports.foo = function foo() {}', () => {
 51 |       expectTransform('exports.foo = function foo() {};').toReturn('export function foo() {}');
 52 |     });
 53 | 
 54 |     it('should ignore function export when function name does not match with exported name', () => {
 55 |       expectNoChange('exports.foo = function bar() {};');
 56 |     });
 57 | 
 58 |     it('should convert exports.foo = arrow function', () => {
 59 |       expectTransform(
 60 |         'exports.foo = () => {\n' +
 61 |         '  return 1;\n' +
 62 |         '};'
 63 |       ).toReturn(
 64 |         'export function foo() {\n' +
 65 |         '  return 1;\n' +
 66 |         '}'
 67 |       );
 68 |     });
 69 | 
 70 |     it('should convert exports.foo = arrow function short form', () => {
 71 |       expectTransform(
 72 |         'exports.foo = x => x;'
 73 |       ).toReturn(
 74 |         'export function foo(x) {\n' +
 75 |         '  return x;\n' +
 76 |         '}'
 77 |       );
 78 |     });
 79 | 
 80 |     it('should convert exports.Foo = class {};', () => {
 81 |       expectTransform('exports.Foo = class {};').toReturn('export class Foo {}');
 82 |     });
 83 | 
 84 |     it('should convert exports.Foo = class Foo {};', () => {
 85 |       expectTransform('exports.Foo = class Foo {};').toReturn('export class Foo {}');
 86 |     });
 87 | 
 88 |     it('should ignore class export when class name does not match with exported name', () => {
 89 |       expectNoChange('exports.Foo = class Bar {};');
 90 |     });
 91 | 
 92 |     it('should convert exports.foo = foo;', () => {
 93 |       expectTransform('exports.foo = foo;').toReturn('export {foo};');
 94 |     });
 95 | 
 96 |     it('should convert exports.foo = bar;', () => {
 97 |       expectTransform('exports.foo = bar;').toReturn('export {bar as foo};');
 98 |     });
 99 | 
100 |     it('should export undefined & NaN like any other identifier', () => {
101 |       expectTransform('exports.foo = undefined;').toReturn('export {undefined as foo};');
102 |       expectTransform('exports.foo = NaN;').toReturn('export {NaN as foo};');
103 |     });
104 | 
105 |     it('should convert exports.foo = <literal> to export var', () => {
106 |       expectTransform('exports.foo = 123;').toReturn('export var foo = 123;');
107 |       expectTransform('exports.foo = {a: 1, b: 2};').toReturn('export var foo = {a: 1, b: 2};');
108 |       expectTransform('exports.foo = [1, 2, 3];').toReturn('export var foo = [1, 2, 3];');
109 |       expectTransform('exports.foo = "Hello";').toReturn('export var foo = "Hello";');
110 |       expectTransform('exports.foo = null;').toReturn('export var foo = null;');
111 |       expectTransform('exports.foo = true;').toReturn('export var foo = true;');
112 |       expectTransform('exports.foo = false;').toReturn('export var foo = false;');
113 |     });
114 | 
115 |     it('should ignore exports.foo inside statements', () => {
116 |       expectNoChange(
117 |         'if (true) {\n' +
118 |         '  exports.foo = function() {};\n' +
119 |         '}'
120 |       );
121 |     });
122 |   });
123 | 
124 |   describe('comments', () => {
125 |     it('should preserve comments before default export', () => {
126 |       expectTransform(
127 |         '// Comments\n' +
128 |         'module.exports = function() {};'
129 |       ).toReturn(
130 |         '// Comments\n' +
131 |         'export default function() {};'
132 |       );
133 |     });
134 | 
135 |     it('should preserve comments before named function export', () => {
136 |       expectTransform(
137 |         '// Comments\n' +
138 |         'exports.foo = function() {};'
139 |       ).toReturn(
140 |         '// Comments\n' +
141 |         'export function foo() {}'
142 |       );
143 |     });
144 | 
145 |     it('should preserve comments before named class export', () => {
146 |       expectTransform(
147 |         '// Comments\n' +
148 |         'exports.Foo = class {};'
149 |       ).toReturn(
150 |         '// Comments\n' +
151 |         'export class Foo {}'
152 |       );
153 |     });
154 | 
155 |     it('should preserve comments before identifier export', () => {
156 |       expectTransform(
157 |         '// Comments\n' +
158 |         'exports.foo = foo;'
159 |       ).toReturn(
160 |         '// Comments\n' +
161 |         'export {foo};'
162 |       );
163 |     });
164 | 
165 |     it('should preserve comments before named literal value export', () => {
166 |       expectTransform(
167 |         '// Comments\n' +
168 |         'exports.FOO = 123;'
169 |       ).toReturn(
170 |         '// Comments\n' +
171 |         'export var FOO = 123;'
172 |       );
173 |     });
174 |   });
175 | });
176 | 


--------------------------------------------------------------------------------
/test/transform/commonjs/importCommonjsTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['commonjs']);
  3 | 
  4 | describe('Import CommonJS', () => {
  5 |   describe('default import', () => {
  6 |     it('should convert basic var/let/const with require()', () => {
  7 |       expectTransform('var   foo = require("foo");').toReturn('import foo from "foo";');
  8 |       expectTransform('const foo = require("foo");').toReturn('import foo from "foo";');
  9 |       expectTransform('let   foo = require("foo");').toReturn('import foo from "foo";');
 10 |     });
 11 | 
 12 |     it('should do nothing with var that contains no require()', () => {
 13 |       expectNoChange('var foo = "bar";');
 14 |       expectNoChange('var foo;');
 15 |     });
 16 | 
 17 |     it('should do nothing with require() that does not have a single string argument', () => {
 18 |       expectNoChange('var foo = require();');
 19 |       expectNoChange('var foo = require("foo", {});');
 20 |       expectNoChange('var foo = require(bar);');
 21 |       expectNoChange('var foo = require(123);');
 22 |     });
 23 | 
 24 |     it('should convert var with multiple require() calls', () => {
 25 |       expectTransform(
 26 |         'var foo = require("foo"), bar = require("bar");'
 27 |       ).toReturn(
 28 |         'import foo from "foo";\n' +
 29 |         'import bar from "bar";'
 30 |       );
 31 |     });
 32 | 
 33 |     it('should convert var/let/const with intermixed require() calls and normal initializations', () => {
 34 |       expectTransform(
 35 |         'var foo = require("foo"), bar = 15;'
 36 |       ).toReturn(
 37 |         'import foo from "foo";\n' +
 38 |         'var bar = 15;'
 39 |       );
 40 | 
 41 |       expectTransform(
 42 |         'let abc, foo = require("foo")'
 43 |       ).toReturn(
 44 |         'let abc;\n' +
 45 |         'import foo from "foo";'
 46 |       );
 47 | 
 48 |       expectTransform(
 49 |         'const greeting = "hello", foo = require("foo");'
 50 |       ).toReturn(
 51 |         'const greeting = "hello";\n' +
 52 |         'import foo from "foo";'
 53 |       );
 54 |     });
 55 | 
 56 |     // It would be nice to preserve the combined declarations,
 57 |     // but this kind of intermixed vars should really be a rare edge case.
 58 |     it('does not need to preserve combined variable declarations', () => {
 59 |       expectTransform(
 60 |         'var foo = require("foo"), bar = 1, baz = 2;'
 61 |       ).toReturn(
 62 |         'import foo from "foo";\n' +
 63 |         'var bar = 1;\n' +
 64 |         'var baz = 2;'
 65 |       );
 66 |     });
 67 | 
 68 |     it('should ignore require calls inside statements', () => {
 69 |       expectNoChange(
 70 |         'if (true) {\n' +
 71 |         '  var foo = require("foo");\n' +
 72 |         '}'
 73 |       ).withWarnings([
 74 |         {line: 2, msg: 'import can only be at root level', type: 'commonjs'}
 75 |       ]);
 76 |     });
 77 | 
 78 |     it('should treat require().default as default import', () => {
 79 |       expectTransform(
 80 |         'var foo = require("foolib").default;'
 81 |       ).toReturn(
 82 |         'import foo from "foolib";'
 83 |       );
 84 |     });
 85 | 
 86 |     it('should treat {default: foo} destructuring as default import', () => {
 87 |       expectTransform(
 88 |         'var {default: foo} = require("foolib");'
 89 |       ).toReturn(
 90 |         'import foo from "foolib";'
 91 |       );
 92 |     });
 93 | 
 94 |     it('should recognize default import inside several destructurings', () => {
 95 |       expectTransform(
 96 |         'var {default: foo, bar: bar} = require("foolib");'
 97 |       ).toReturn(
 98 |         'import foo, {bar} from "foolib";'
 99 |       );
100 |     });
101 |   });
102 | 
103 |   describe('named import', () => {
104 |     it('should convert foo = require().foo to named import', () => {
105 |       expectTransform(
106 |         'var foo = require("foolib").foo;'
107 |       ).toReturn(
108 |         'import {foo} from "foolib";'
109 |       );
110 |     });
111 | 
112 |     it('should convert bar = require().foo to aliased named import', () => {
113 |       expectTransform(
114 |         'var bar = require("foolib").foo;'
115 |       ).toReturn(
116 |         'import {foo as bar} from "foolib";'
117 |       );
118 |     });
119 | 
120 |     it('should convert simple object destructuring to named import', () => {
121 |       expectTransform(
122 |         'var {foo} = require("foolib");'
123 |       ).toReturn(
124 |         'import {foo} from "foolib";'
125 |       );
126 |     });
127 | 
128 |     it('should convert aliased object destructuring to named import', () => {
129 |       expectTransform(
130 |         'var {foo: bar} = require("foolib");'
131 |       ).toReturn(
132 |         'import {foo as bar} from "foolib";'
133 |       );
134 |     });
135 | 
136 |     it('should convert multi-field object destructurings to named imports', () => {
137 |       expectTransform(
138 |         'var {foo, bar: myBar, baz} = require("foolib");'
139 |       ).toReturn(
140 |         'import {foo, bar as myBar, baz} from "foolib";'
141 |       );
142 |     });
143 | 
144 |     it('should ignore array destructuring', () => {
145 |       expectNoChange(
146 |         'var [a, b, c] = require("foolib");'
147 |       );
148 |     });
149 | 
150 |     it('should ignore nested object destructuring', () => {
151 |       expectNoChange(
152 |         'var {foo: {bar}} = require("foolib");'
153 |       );
154 |     });
155 | 
156 |     it('should ignore destructuring of require().foo', () => {
157 |       expectNoChange(
158 |         'var {foo} = require("foolib").foo;'
159 |       );
160 |     });
161 |   });
162 | 
163 |   describe('comments', () => {
164 |     it('should preserve comments before var declaration', () => {
165 |       expectTransform(
166 |         '// Comments\n' +
167 |         'var foo = require("foo");'
168 |       ).toReturn(
169 |         '// Comments\n' +
170 |         'import foo from "foo";'
171 |       );
172 |     });
173 |   });
174 | 
175 |   // Not yet supported things...
176 | 
177 |   it('should not convert assignment of require() call', () => {
178 |     expectNoChange('foo = require("foo");');
179 |   });
180 | 
181 |   it('should not convert unassigned require() call', () => {
182 |     expectNoChange('require("foo");');
183 |   });
184 | });
185 | 


--------------------------------------------------------------------------------
/test/transform/destructParamTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['destruct-param']);
  3 | 
  4 | describe('Destruct function param', () => {
  5 |   it('should transform when only props are accessed', () => {
  6 |     expectTransform(
  7 |       'function fn(cfg) {\n' +
  8 |       '  console.log(cfg.foo, cfg.bar);\n' +
  9 |       '}'
 10 |     ).toReturn(
 11 |       'function fn({foo, bar}) {\n' +
 12 |       '  console.log(foo, bar);\n' +
 13 |       '}'
 14 |     );
 15 |   });
 16 | 
 17 |   it('should transform when the same prop accessed multiple times', () => {
 18 |     expectTransform(
 19 |       'function fn(cfg) {\n' +
 20 |       '  console.log(cfg.foo, cfg.bar, cfg.foo);\n' +
 21 |       '}'
 22 |     ).toReturn(
 23 |       'function fn({foo, bar}) {\n' +
 24 |       '  console.log(foo, bar, foo);\n' +
 25 |       '}'
 26 |     );
 27 |   });
 28 | 
 29 |   it('should not transform when re-defined as variable', () => {
 30 |     expectNoChange(
 31 |       'function fn(cfg) {\n' +
 32 |       '  var cfg;\n' +
 33 |       '  console.log(cfg.foo, cfg.bar);\n' +
 34 |       '}'
 35 |     );
 36 |   });
 37 | 
 38 |   it('should not transform when is used without props-access', () => {
 39 |     expectNoChange(
 40 |       'function fn(cfg) {\n' +
 41 |       '  console.log(cfg, cfg.foo, cfg.bar);\n' +
 42 |       '}'
 43 |     );
 44 |   });
 45 | 
 46 |   it('should not transform computed props-access', () => {
 47 |     expectNoChange(
 48 |       'function fn(cfg) {\n' +
 49 |       '  console.log(cfg, cfg["foo-hoo"], cfg["bar-haa"]);\n' +
 50 |       '}'
 51 |     );
 52 |   });
 53 | 
 54 |   it('should not transform when props are assigned', () => {
 55 |     expectNoChange(
 56 |       'function fn(cfg) {\n' +
 57 |       '  cfg.foo = 1;\n' +
 58 |       '  console.log(cfg.foo, cfg.bar);\n' +
 59 |       '}'
 60 |     );
 61 |   });
 62 | 
 63 |   it('should not transform when props are updated', () => {
 64 |     expectNoChange(
 65 |       'function fn(cfg) {\n' +
 66 |       '  cfg.foo++;\n' +
 67 |       '  console.log(cfg.foo, cfg.bar);\n' +
 68 |       '}'
 69 |     );
 70 |   });
 71 | 
 72 |   it('should not transform when props are methods', () => {
 73 |     expectNoChange(
 74 |       'function fn(cfg) {\n' +
 75 |       '  console.log(cfg.foo(), cfg.bar);\n' +
 76 |       '}'
 77 |     );
 78 |   });
 79 | 
 80 |   it('should not transform when props are keywords', () => {
 81 |     expectNoChange(
 82 |       'function fn(cfg) {\n' +
 83 |       '  console.log(cfg.let, cfg.for);\n' +
 84 |       '}'
 85 |     );
 86 |   });
 87 | 
 88 |   it('should not transform when param with name of prop already exists', () => {
 89 |     expectNoChange(
 90 |       'function fn(cfg, bar) {\n' +
 91 |       '  console.log(cfg.foo, cfg.bar);\n' +
 92 |       '}'
 93 |     );
 94 |   });
 95 | 
 96 |   it('should not transform when variable with name of prop already exists', () => {
 97 |     expectNoChange(
 98 |       'function fn(cfg) {\n' +
 99 |       '  var foo = 10;\n' +
100 |       '  console.log(cfg.foo, cfg.bar);\n' +
101 |       '}'
102 |     );
103 |   });
104 | 
105 |   it('should not transform when import with name of prop already exists', () => {
106 |     expectNoChange(
107 |       'import foo from "./myFooModule";\n' +
108 |       'function fn(cfg) {\n' +
109 |       '  console.log(cfg.foo, cfg.bar);\n' +
110 |       '}'
111 |     );
112 |   });
113 | 
114 |   it('should not transform when shadowing a global variable', () => {
115 |     expectNoChange(
116 |       'function fn(cfg) {\n' +
117 |       '  console.log(cfg.window);\n' +
118 |       '  window.open("_blank");\n' +
119 |       '}'
120 |     );
121 |   });
122 | 
123 |   it('should not transform when shadowing a global variable in separate function', () => {
124 |     expectNoChange(
125 |       'function fn(cfg) {\n' +
126 |       '  function fn1() {\n' +
127 |       '    console.log(cfg.window);\n' +
128 |       '  }\n' +
129 |       '  function fn2() {\n' +
130 |       '    window.open("_blank");\n' +
131 |       '  }\n' +
132 |       '}'
133 |     );
134 |   });
135 | 
136 |   it('should not transform already destructed param', () => {
137 |     expectNoChange(
138 |       'function fn({cfg, cfg2}) {\n' +
139 |       '  console.log(cfg.foo, cfg.bar);\n' +
140 |       '}'
141 |     );
142 |   });
143 | 
144 |   it.skip('should not transform second param when this results in conflict', () => {
145 |     expectTransform(
146 |       'function fn(a, b) {\n' +
147 |       '  console.log(a.foo, b.foo);\n' +
148 |       '}'
149 |     ).toReturn(
150 |       'function fn({foo}, b) {\n' +
151 |       '  console.log(foo, b.foo);\n' +
152 |       '}'
153 |     );
154 |   });
155 | 
156 |   it.skip('should not perform second transform when it results in conflict', () => {
157 |     expectTransform(
158 |       'function fn(a) {\n' +
159 |       '  console.log(a.foo);\n' +
160 |       '  function fn(b) {\n' +
161 |       '    console.log(a.foo, b.foo);\n' +
162 |       '  }\n' +
163 |       '}'
164 |     ).toReturn(
165 |       'function fn({foo}) {\n' +
166 |       '  console.log(foo);\n' +
167 |       '  function fn(b) {\n' +
168 |       '    console.log(foo, b.foo);\n' +
169 |       '  }\n' +
170 |       '}'
171 |     );
172 |   });
173 | 
174 |   it('should transform when MAX_PROPS props', () => {
175 |     expectTransform(
176 |       'function fn(cfg) {\n' +
177 |       '  console.log(cfg.p1, cfg.p2, cfg.p3, cfg.p4);\n' +
178 |       '}'
179 |     ).toReturn(
180 |       'function fn({p1, p2, p3, p4}) {\n' +
181 |       '  console.log(p1, p2, p3, p4);\n' +
182 |       '}'
183 |     );
184 |   });
185 | 
186 |   it('should not transform more than MAX_PROPS props', () => {
187 |     expectNoChange(
188 |       'function fn(cfg) {\n' +
189 |       '  console.log(cfg.p1, cfg.p2, cfg.p3, cfg.p4, cfg.p5);\n' +
190 |       '}'
191 |     ).withWarnings([
192 |       {line: 1, msg: '5 different props found, will not transform more than 4', type: 'destruct-param'}
193 |     ]);
194 |   });
195 | });
196 | 


--------------------------------------------------------------------------------
/test/transform/ecmaVersionTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectNoChange} = createTestHelpers([
 3 |   'class',
 4 |   'template',
 5 |   'arrow',
 6 |   'let',
 7 |   'default-param',
 8 |   'arg-spread',
 9 |   'obj-method',
10 |   'obj-shorthand',
11 |   'no-strict',
12 |   'commonjs',
13 |   'exponent',
14 | ]);
15 | 
16 | describe('ECMAScript version', () => {
17 |   it('supports optional catch binding in ECMAScript 2019', () => {
18 |     expectNoChange('try { ohNo(); } catch { console.log("error!"); }');
19 |   });
20 | 
21 |   it('supports optional chaining in ECMAScript 2020', () => {
22 |     expectNoChange('foo?.bar();');
23 |   });
24 | 
25 |   it('supports numeric separators in ECMAScript 2021', () => {
26 |     expectNoChange('const nr = 10_000_000;');
27 |   });
28 | 
29 |   it('supports private class fields ECMAScript 2022', () => {
30 |     expectNoChange('class Foo { #field = 42; #method() {} }');
31 |   });
32 | });
33 | 


--------------------------------------------------------------------------------
/test/transform/exponentTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectTransform, expectNoChange} = createTestHelpers(['exponent']);
 3 | 
 4 | describe('Exponentiation operator', () => {
 5 |   it('should convert Math.pow()', () => {
 6 |     expectTransform(
 7 |       'result = Math.pow(a, b);'
 8 |     ).toReturn(
 9 |       'result = a ** b;'
10 |     );
11 |   });
12 | 
13 |   it('should parenthesize arguments when needed', () => {
14 |     expectTransform(
15 |       'result = Math.pow(a + 1, b + 2);'
16 |     ).toReturn(
17 |       'result = (a + 1) ** (b + 2);'
18 |     );
19 |   });
20 | 
21 |   it('should parenthesize multiplication', () => {
22 |     expectTransform(
23 |       'Math.pow(x * 2, 2);'
24 |     ).toReturn(
25 |       '(x * 2) ** 2;'
26 |     );
27 |   });
28 | 
29 |   it('should not convert Math.pow() without exactly two arguments', () => {
30 |     expectNoChange('Math.pow();');
31 |     expectNoChange('Math.pow(1);');
32 |     expectNoChange('Math.pow(1, 2, 3);');
33 |   });
34 | });
35 | 


--------------------------------------------------------------------------------
/test/transform/includesTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['includes']);
  3 | 
  4 | describe('indexOf() to includes()', () => {
  5 |   it('should replace !== -1 with includes()', () => {
  6 |     expectTransform(
  7 |       'if (array.indexOf(foo) !== -1) { /* */ }'
  8 |     ).toReturn(
  9 |       'if (array.includes(foo)) { /* */ }'
 10 |     );
 11 |   });
 12 | 
 13 |   it('should NOT transform !== 0', () => {
 14 |     expectNoChange(
 15 |       'if (array.indexOf(foo) !== 0) { /* */ }'
 16 |     );
 17 |   });
 18 | 
 19 |   it('should replace === -1 with !includes()', () => {
 20 |     expectTransform(
 21 |       'if (array.indexOf(foo) === -1) { /* */ }'
 22 |     ).toReturn(
 23 |       'if (!array.includes(foo)) { /* */ }'
 24 |     );
 25 |   });
 26 | 
 27 |   it('should NOT transform === 0', () => {
 28 |     expectNoChange(
 29 |       'if (array.indexOf(foo) === 0) { /* */ }'
 30 |     );
 31 |   });
 32 | 
 33 |   it('should replace != -1 with includes()', () => {
 34 |     expectTransform(
 35 |       'if (array.indexOf(foo) != -1) { /* */ }'
 36 |     ).toReturn(
 37 |       'if (array.includes(foo)) { /* */ }'
 38 |     );
 39 |   });
 40 | 
 41 |   it('should NOT transform != 0', () => {
 42 |     expectNoChange(
 43 |       'if (array.indexOf(foo) != 0) { /* */ }'
 44 |     );
 45 |   });
 46 | 
 47 |   it('should replace == -1 with !includes()', () => {
 48 |     expectTransform(
 49 |       'if (array.indexOf(foo) == -1) { /* */ }'
 50 |     ).toReturn(
 51 |       'if (!array.includes(foo)) { /* */ }'
 52 |     );
 53 |   });
 54 | 
 55 |   it('should NOT transform == 0', () => {
 56 |     expectNoChange(
 57 |       'if (array.indexOf(foo) == 0) { /* */ }'
 58 |     );
 59 |   });
 60 | 
 61 |   it('should replace > -1 with includes()', () => {
 62 |     expectTransform(
 63 |       'if (array.indexOf(foo) > -1) { /* */ }'
 64 |     ).toReturn(
 65 |       'if (array.includes(foo)) { /* */ }'
 66 |     );
 67 |   });
 68 | 
 69 |   it('should NOT transform > 0', () => {
 70 |     expectNoChange(
 71 |       'if (array.indexOf(foo) > 0) { /* */ }'
 72 |     );
 73 |   });
 74 | 
 75 |   it('should NOT transform >= -1', () => {
 76 |     expectNoChange(
 77 |       'if (array.indexOf(foo) >= -1) { /* */ }'
 78 |     );
 79 |   });
 80 | 
 81 |   it('should replace >= 0 with includes()', () => {
 82 |     expectTransform(
 83 |       'if (array.indexOf(foo) >= 0) { /* */ }'
 84 |     ).toReturn(
 85 |       'if (array.includes(foo)) { /* */ }'
 86 |     );
 87 |   });
 88 | 
 89 |   it('should NOT transform < -1', () => {
 90 |     expectNoChange(
 91 |       'if (array.indexOf(foo) < -1) { /* */ }'
 92 |     );
 93 |   });
 94 | 
 95 |   it('should replace < 0 with !includes()', () => {
 96 |     expectTransform(
 97 |       'if (array.indexOf(foo) < 0) { /* */ }'
 98 |     ).toReturn(
 99 |       'if (!array.includes(foo)) { /* */ }'
100 |     );
101 |   });
102 | 
103 |   it('should NOT transform <= -1', () => {
104 |     expectNoChange(
105 |       'if (array.indexOf(foo) <= -1) { /* */ }'
106 |     );
107 |   });
108 | 
109 |   it('should NOT transform <= 0', () => {
110 |     expectNoChange(
111 |       'if (array.indexOf(foo) <= 0) { /* */ }'
112 |     );
113 |   });
114 | 
115 |   describe('reversed operands', () => {
116 |     it('should transform -1 !== indexOf()', () => {
117 |       expectTransform(
118 |         'if (-1 !== array.indexOf(foo)) { /* */ }'
119 |       ).toReturn(
120 |         'if (array.includes(foo)) { /* */ }'
121 |       );
122 |     });
123 | 
124 |     it('should transform -1 === indexOf()', () => {
125 |       expectTransform(
126 |         'if (-1 === array.indexOf(foo)) { /* */ }'
127 |       ).toReturn(
128 |         'if (!array.includes(foo)) { /* */ }'
129 |       );
130 |     });
131 | 
132 |     it('should transform -1 < indexOf()', () => {
133 |       expectTransform(
134 |         'if (-1 < array.indexOf(foo)) { /* */ }'
135 |       ).toReturn(
136 |         'if (array.includes(foo)) { /* */ }'
137 |       );
138 |     });
139 | 
140 |     it('should transform 0 <= indexOf()', () => {
141 |       expectTransform(
142 |         'if (0 <= array.indexOf(foo)) { /* */ }'
143 |       ).toReturn(
144 |         'if (array.includes(foo)) { /* */ }'
145 |       );
146 |     });
147 | 
148 |     it('should transform 0 > indexOf()', () => {
149 |       expectTransform(
150 |         'if (0 > array.indexOf(foo)) { /* */ }'
151 |       ).toReturn(
152 |         'if (!array.includes(foo)) { /* */ }'
153 |       );
154 |     });
155 | 
156 |     it('should NOT transform 0 >= indexOf()', () => {
157 |       expectNoChange(
158 |         'if (0 >= array.indexOf(foo)) { /* */ }'
159 |       );
160 |     });
161 |   });
162 | 
163 |   describe('additional checks', () => {
164 |     it('should allow complex array expression', () => {
165 |       expectTransform(
166 |         'if ([1,2,3].indexOf(foo) !== -1) { /* */ }'
167 |       ).toReturn(
168 |         'if ([1,2,3].includes(foo)) { /* */ }'
169 |       );
170 |     });
171 | 
172 |     it('should allow complex string expression', () => {
173 |       expectTransform(
174 |         'if (("foo" + "bar").indexOf(foo) !== -1) { /* */ }'
175 |       ).toReturn(
176 |         'if (("foo" + "bar").includes(foo)) { /* */ }'
177 |       );
178 |     });
179 | 
180 |     it('should allow complex searchElement expression', () => {
181 |       expectTransform(
182 |         'if (array.indexOf(a + b + c) !== -1) { /* */ }'
183 |       ).toReturn(
184 |         'if (array.includes(a + b + c)) { /* */ }'
185 |       );
186 |     });
187 | 
188 |     it('should NOT transform indexOf() with multiple parameters', () => {
189 |       expectNoChange(
190 |         'if (array.indexOf(a, b) !== -1) { /* */ }'
191 |       );
192 |     });
193 |   });
194 | });
195 | 


--------------------------------------------------------------------------------
/test/transform/jsxTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectTransform} = createTestHelpers([
 3 |   'class',
 4 |   'template',
 5 |   'arrow',
 6 |   'let',
 7 |   'default-param',
 8 |   'arg-spread',
 9 |   'obj-method',
10 |   'obj-shorthand',
11 |   'no-strict',
12 |   'commonjs',
13 |   'exponent',
14 | ]);
15 | 
16 | describe('JSX support', () => {
17 |   it('should support self-closing element', () => {
18 |     expectTransform(
19 |       'var foo = <div/>;'
20 |     ).toReturn(
21 |       'const foo = <div/>;'
22 |     );
23 |   });
24 | 
25 |   it('should support attributes', () => {
26 |     expectTransform(
27 |       'var foo = <div foo="hello" bar={2}/>;'
28 |     ).toReturn(
29 |       'const foo = <div foo="hello" bar={2}/>;'
30 |     );
31 |   });
32 | 
33 |   it('should support spread attributes', () => {
34 |     expectTransform(
35 |       'var foo = <div {...attrs}/>;'
36 |     ).toReturn(
37 |       'const foo = <div {...attrs}/>;'
38 |     );
39 |   });
40 | 
41 |   it('should support nested elements', () => {
42 |     expectTransform(
43 |       'var foo = <div>\n' +
44 |       '    <Foo/>\n' +
45 |       '    <Bar/>\n' +
46 |       '</div>;'
47 |     ).toReturn(
48 |       'const foo = <div>\n' +
49 |       '    <Foo/>\n' +
50 |       '    <Bar/>\n' +
51 |       '</div>;'
52 |     );
53 |   });
54 | 
55 |   it('should support member-expressions as element name', () => {
56 |     expectTransform(
57 |       'var foo = <Foo.bar/>;'
58 |     ).toReturn(
59 |       'const foo = <Foo.bar/>;'
60 |     );
61 |   });
62 | 
63 |   it('should support XML namespaces', () => {
64 |     expectTransform(
65 |       'var foo = <xml:foo/>;'
66 |     ).toReturn(
67 |       'const foo = <xml:foo/>;'
68 |     );
69 |   });
70 | 
71 |   it('should support content', () => {
72 |     expectTransform(
73 |       'var foo = <div>Hello {a + b}</div>;'
74 |     ).toReturn(
75 |       'const foo = <div>Hello {a + b}</div>;'
76 |     );
77 |   });
78 | 
79 |   it('should support empty content expressions', () => {
80 |     expectTransform(
81 |       'var foo = <div> {/* some comments */} </div>;'
82 |     ).toReturn(
83 |       'const foo = <div> {/* some comments */} </div>;'
84 |     );
85 |   });
86 | 
87 |   it('should support JSX fragments', () => {
88 |     expectTransform(
89 |       'var foo = <>{a}</>;'
90 |     ).toReturn(
91 |       'const foo = <>{a}</>;'
92 |     );
93 |   });
94 | });
95 | 


--------------------------------------------------------------------------------
/test/transform/multiVarTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['multi-var']);
  3 | 
  4 | describe('Multi-var', () => {
  5 |   describe('with only one variable per declaration', () => {
  6 |     it('should not change anything', () => {
  7 |       expectTransform(
  8 |         'var x;'
  9 |       ).toReturn(
 10 |         'var x;'
 11 |       );
 12 |     });
 13 |   });
 14 | 
 15 |   describe('with only uninitialized variables', () => {
 16 |     it('should split into separate declarations', () => {
 17 |       expectTransform(
 18 |         'var x,y;'
 19 |       ).toReturn(
 20 |         'var x;\n' +
 21 |         'var y;'
 22 |       );
 23 |     });
 24 |   });
 25 | 
 26 |   describe('with uninitialized and initialized variables', () => {
 27 |     it('should split into separate declarations', () => {
 28 |       expectTransform(
 29 |         'var x,y=100;'
 30 |       ).toReturn(
 31 |         'var x;\n' +
 32 |         'var y=100;'
 33 |       );
 34 |     });
 35 |   });
 36 | 
 37 |   describe('with various type of declarations', () => {
 38 |     it('should split into separate declarations', () => {
 39 |       expectTransform(
 40 |         'var x,y=100;\n' +
 41 |         'let a,b=123;\n' +
 42 |         'const c=12,d=234'
 43 |       ).toReturn(
 44 |         'var x;\n' +
 45 |         'var y=100;\n' +
 46 |         'let a;\n' +
 47 |         'let b=123;\n' +
 48 |         'const c=12;\n' +
 49 |         'const d=234;'
 50 |       );
 51 |     });
 52 |   });
 53 | 
 54 |   describe('with inline comment', () => {
 55 |     it('should split into separate declarations', () => {
 56 |       expectTransform(
 57 |         'var x,y=100;// hello'
 58 |       ).toReturn(
 59 |         'var x;// hello\n' +
 60 |         'var y=100;'
 61 |       );
 62 |     });
 63 |   });
 64 | 
 65 |   describe('with block comment', () => {
 66 |     it('should split into separate declarations(comment before declaration)', () => {
 67 |       expectTransform(
 68 |         '/* hello */var x,y=100;'
 69 |       ).toReturn(
 70 |         '/* hello */var x;\n' +
 71 |         'var y=100;'
 72 |       );
 73 |     });
 74 | 
 75 |     it('should split into separate declarations(comment after declaration)', () => {
 76 |       expectTransform(
 77 |         'var x,y=100;/* hello */'
 78 |       ).toReturn(
 79 |         'var x;/* hello */\n' +
 80 |         'var y=100;'
 81 |       );
 82 |     });
 83 | 
 84 |     it('should split into separate declarations(comment inside declaration)', () => {
 85 |       expectTransform(
 86 |         'var x,/* hello */y=100;'
 87 |       ).toReturn(
 88 |         'var x;\n' +
 89 |         'var /* hello */y=100;'
 90 |       );
 91 |     });
 92 |   });
 93 | 
 94 |   describe('inside case statement', () => {
 95 |     it('should split into separate declarations', () => {
 96 |       expectTransform(
 97 |         'switch (nr) {\n' +
 98 |         '  case 1:\n' +
 99 |         '    var a=1, b=2;\n' +
100 |         '}'
101 |       ).toReturn(
102 |         'switch (nr) {\n' +
103 |         '  case 1:\n' +
104 |         '    var a=1;\n' +
105 |         '    var b=2;\n' +
106 |         '}'
107 |       );
108 |     });
109 |   });
110 | 
111 |   describe('when var can not be split', () => {
112 |     it('should not split in for-loop head', () => {
113 |       expectNoChange(
114 |         'for (var i=0,j=0; i<j; i++) j++;'
115 |       ).withWarnings([
116 |         {line: 1, msg: 'Unable to split var statement in a ForStatement', type: 'multi-var'}
117 |       ]);
118 |     });
119 | 
120 |     it('should not split when not in block statement', () => {
121 |       expectNoChange(
122 |         'if (true) var a=1, b=2;'
123 |       ).withWarnings([
124 |         {line: 1, msg: 'Unable to split var statement in a IfStatement', type: 'multi-var'}
125 |       ]);
126 |     });
127 |   });
128 | });
129 | 


--------------------------------------------------------------------------------
/test/transform/noStrictTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectTransform, expectNoChange} = createTestHelpers(['no-strict']);
 3 | 
 4 | describe('Removal of "use strict"', () => {
 5 |   it('should remove statement with "use strict" string', () => {
 6 |     expectTransform('"use strict";').toReturn('');
 7 |     expectTransform('\'use strict\';').toReturn('');
 8 |   });
 9 | 
10 |   it('should remove the whole line where "use strict" used to be', () => {
11 |     expectTransform(
12 |       '"use strict";\n' +
13 |       'foo();'
14 |     ).toReturn(
15 |       'foo();'
16 |     );
17 | 
18 |     expectTransform(
19 |       'foo();\n' +
20 |       '"use strict";\n' +
21 |       'bar();'
22 |     ).toReturn(
23 |       'foo();\n' +
24 |       'bar();'
25 |     );
26 |   });
27 | 
28 |   it('should not remove comments before "use strict"', () => {
29 |     expectTransform(
30 |       '// comment\n' +
31 |       '"use strict";\n' +
32 |       'bar();'
33 |     ).toReturn(
34 |       '// comment\n' +
35 |       'bar();'
36 |     );
37 |   });
38 | 
39 |   it('should preserve comments when no other code besides "use strict"', () => {
40 |     expectTransform(
41 |       '// some comment\n' +
42 |       '"use strict";'
43 |     ).toReturn(
44 |       '// some comment\n'
45 |     );
46 |   });
47 | 
48 |   it('should keep "use strict" used inside other code', () => {
49 |     expectNoChange('x = "use strict";');
50 |     expectNoChange('foo("use strict");');
51 |   });
52 | });
53 | 


--------------------------------------------------------------------------------
/test/transform/objMethodTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['obj-method']);
  3 | 
  4 | describe('Object methods', () => {
  5 |   it('should convert a function inside an object to method', () => {
  6 |     expectTransform(
  7 |       '({\n' +
  8 |       '  someMethod: function(a, b, c) {\n' +
  9 |       '    return a + b + c;\n' +
 10 |       '  }\n' +
 11 |       '});'
 12 |     ).toReturn(
 13 |       '({\n' +
 14 |       '  someMethod(a, b, c) {\n' +
 15 |       '    return a + b + c;\n' +
 16 |       '  }\n' +
 17 |       '});'
 18 |     ).withoutWarnings();
 19 |   });
 20 | 
 21 |   it('should ignore non-function properties of object', () => {
 22 |     expectTransform(
 23 |       '({\n' +
 24 |       '  foo: 123,\n' +
 25 |       '  method1: function() {\n' +
 26 |       '  },\n' +
 27 |       '  bar: [],\n' +
 28 |       '  method2: function() {\n' +
 29 |       '  },\n' +
 30 |       '});'
 31 |     ).toReturn(
 32 |       '({\n' +
 33 |       '  foo: 123,\n' +
 34 |       '  method1() {\n' +
 35 |       '  },\n' +
 36 |       '  bar: [],\n' +
 37 |       '  method2() {\n' +
 38 |       '  },\n' +
 39 |       '});'
 40 |     ).withoutWarnings();
 41 |   });
 42 | 
 43 |   it('should convert function properties in nested object literal', () => {
 44 |     expectTransform(
 45 |       '({\n' +
 46 |       '  nested: {\n' +
 47 |       '    method: function() {\n' +
 48 |       '    }\n' +
 49 |       '  }\n' +
 50 |       '});'
 51 |     ).toReturn(
 52 |       '({\n' +
 53 |       '  nested: {\n' +
 54 |       '    method() {\n' +
 55 |       '    }\n' +
 56 |       '  }\n' +
 57 |       '});'
 58 |     ).withoutWarnings();
 59 |   });
 60 | 
 61 |   it('should not convert named function expressions', () => {
 62 |     expectNoChange(
 63 |       '({\n' +
 64 |       '  foo: function foo() {\n' +
 65 |       '    return foo();\n' +
 66 |       '  }\n' +
 67 |       '});'
 68 |     ).withWarnings([
 69 |       {line: 2, msg: 'Unable to transform named function', type: 'obj-method'}
 70 |     ]);
 71 |   });
 72 | 
 73 |   it('should not convert computed properties', () => {
 74 |     expectNoChange(
 75 |       '({\n' +
 76 |       '  ["foo" + count]: function() {\n' +
 77 |       '  }\n' +
 78 |       '});'
 79 |     ).withoutWarnings();
 80 |   });
 81 | 
 82 |   it('should not convert string properties', () => {
 83 |     expectNoChange(
 84 |       '({\n' +
 85 |       '  "foo": function() {\n' +
 86 |       '  }\n' +
 87 |       '});'
 88 |     ).withoutWarnings();
 89 |   });
 90 | 
 91 |   it('should not convert numeric properties', () => {
 92 |     expectNoChange(
 93 |       '({\n' +
 94 |       '  123: function() {\n' +
 95 |       '  }\n' +
 96 |       '});'
 97 |     ).withoutWarnings();
 98 |   });
 99 | });
100 | 


--------------------------------------------------------------------------------
/test/transform/objShorthandTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectTransform, expectNoChange} = createTestHelpers(['obj-shorthand']);
 3 | 
 4 | describe('Object shorthands', () => {
 5 |   it('should convert matching key-value entries to shorthand notation', () => {
 6 |     expectTransform('({foo: foo})').toReturn('({foo})');
 7 |   });
 8 | 
 9 |   it('should not convert non-matching key-value entries to shorthand notation', () => {
10 |     expectNoChange('({foo: bar})');
11 |   });
12 | 
13 |   it('should not convert numeric properties to shorthands', () => {
14 |     expectNoChange('({10: 10})');
15 |   });
16 | 
17 |   // One might think we should also convert strings,
18 |   // but there might be some explicit reason why author chose
19 |   // to write his property names as strings,
20 |   // like when using Advanced compilation mode of Google Closure Compiler,
21 |   // where string keys signify that they should not be minified.
22 |   it('should not convert string properties to shorthands', () => {
23 |     expectNoChange('({"foo": foo})');
24 |   });
25 | });
26 | 


--------------------------------------------------------------------------------
/test/transform/restSpreadTest.js:
--------------------------------------------------------------------------------
 1 | import createTestHelpers from '../createTestHelpers';
 2 | const {expectTransform} = createTestHelpers(['let']);
 3 | 
 4 | describe('Rest and Spread support', () => {
 5 |   it('should support object rest in destructuring', () => {
 6 |     expectTransform(
 7 |       'var { foo, ...other } = bar;'
 8 |     ).toReturn(
 9 |       'const { foo, ...other } = bar;'
10 |     );
11 |   });
12 | 
13 |   it('should support object spread', () => {
14 |     expectTransform(
15 |       'var foo = { bar, ...other };'
16 |     ).toReturn(
17 |       'const foo = { bar, ...other };'
18 |     );
19 |   });
20 | });
21 | 


--------------------------------------------------------------------------------
/test/transform/templateTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform, expectNoChange} = createTestHelpers(['template']);
  3 | 
  4 | describe('Template string', () => {
  5 |   it('should not convert non-concatenated strings', () => {
  6 |     expectNoChange('var result = "test";');
  7 |   });
  8 | 
  9 |   it('should not convert non-string binary expressions with + operator', () => {
 10 |     expectNoChange('var result = 1 + 2;');
 11 |     expectNoChange('var result = a + b;');
 12 |   });
 13 | 
 14 |   it('should not convert only string concatenation', () => {
 15 |     expectNoChange('var result = "Hello " + " World!";');
 16 |   });
 17 | 
 18 |   it('should convert string and one variable concatenation', () => {
 19 |     expectTransform(
 20 |       'var result = "Firstname: " + firstname;'
 21 |     ).toReturn(
 22 |       'var result = `Firstname: ${firstname}`;'
 23 |     );
 24 |   });
 25 | 
 26 |   it('should convert string and multiple variables concatenation', () => {
 27 |     expectTransform(
 28 |       'var result = "Fullname: " + firstname + lastname;'
 29 |     ).toReturn(
 30 |       'var result = `Fullname: ${firstname}${lastname}`;'
 31 |     );
 32 |   });
 33 | 
 34 |   it('should convert parenthized string concatenations', () => {
 35 |     expectTransform(
 36 |       '"str1 " + (x + " str2");'
 37 |     ).toReturn(
 38 |       '`str1 ${x} str2`;'
 39 |     );
 40 |   });
 41 | 
 42 |   it('should convert parenthized string concatenations and other concatenations', () => {
 43 |     expectTransform(
 44 |       'x + " str1 " + (y + " str2");'
 45 |     ).toReturn(
 46 |       '`${x} str1 ${y} str2`;'
 47 |     );
 48 |   });
 49 | 
 50 |   it('should convert parenthized non-string concatenations', () => {
 51 |     expectTransform(
 52 |       '(x + y) + " string " + (a + b);'
 53 |     ).toReturn(
 54 |       '`${x + y} string ${a + b}`;'
 55 |     );
 56 |   });
 57 | 
 58 |   it('should convert non-parenthized non-string concatenations', () => {
 59 |     expectTransform(
 60 |       'x + y + " string " + a + b;'
 61 |     ).toReturn(
 62 |       '`${x + y} string ${a}${b}`;'
 63 |     );
 64 |   });
 65 | 
 66 |   it('should convert string and call expressions', () => {
 67 |     expectTransform(
 68 |       'var result = "Firstname: " + person.getFirstname() + "Lastname: " + person.getLastname();'
 69 |     ).toReturn(
 70 |       'var result = `Firstname: ${person.getFirstname()}Lastname: ${person.getLastname()}`;'
 71 |     );
 72 |   });
 73 | 
 74 |   it('should convert string and number literals', () => {
 75 |     expectTransform(
 76 |       '"foo " + 25 + " bar";'
 77 |     ).toReturn(
 78 |       '`foo ${25} bar`;'
 79 |     );
 80 |   });
 81 | 
 82 |   it('should convert string and member-expressions', () => {
 83 |     expectTransform(
 84 |       '"foo " + foo.bar + " bar";'
 85 |     ).toReturn(
 86 |       '`foo ${foo.bar} bar`;'
 87 |     );
 88 |   });
 89 | 
 90 |   it('should escape ` characters', () => {
 91 |     expectTransform(
 92 |       'var result = "Firstname: `" + firstname + "`";'
 93 |     ).toReturn(
 94 |       'var result = `Firstname: \\`${firstname}\\``;'
 95 |     );
 96 |   });
 97 | 
 98 |   it('should leave \\t, \\r, \\n, \\v, \\f, \\b, \\0, \\\\ escaped as is', () => {
 99 |     expectTransform('x = "\\t" + y;').toReturn('x = `\\t${y}`;');
100 |     expectTransform('x = "\\r" + y;').toReturn('x = `\\r${y}`;');
101 |     expectTransform('x = "\\n" + y;').toReturn('x = `\\n${y}`;');
102 |     expectTransform('x = "\\v" + y;').toReturn('x = `\\v${y}`;');
103 |     expectTransform('x = "\\f" + y;').toReturn('x = `\\f${y}`;');
104 |     expectTransform('x = "\\b" + y;').toReturn('x = `\\b${y}`;');
105 |     expectTransform('x = "\\0" + y;').toReturn('x = `\\0${y}`;');
106 |     expectTransform('x = "\\\\" + y;').toReturn('x = `\\\\${y}`;');
107 |   });
108 | 
109 |   it('should leave hex- and unicode-escapes as is', () => {
110 |     expectTransform('x = "\\xA9" + y;').toReturn('x = `\\xA9${y}`;');
111 |     expectTransform('x = "\\u00A9" + y;').toReturn('x = `\\u00A9${y}`;');
112 |   });
113 | 
114 |   it('should eliminate escaping of quotes', () => {
115 |     expectTransform('x = "\\\'" + y;').toReturn('x = `\'${y}`;');
116 |     expectTransform('x = "\\"" + y;').toReturn('x = `"${y}`;');
117 |   });
118 | 
119 |   it('should preserve comments', () => {
120 |     expectTransform(
121 |       'var foo =\n' +
122 |       '    // First comment\n' +
123 |       '    "Firstname: " + fname + ' +
124 |       '    // Second comment\n' +
125 |       '    " Middlename: " + mname +' +
126 |       '    // Third comment\n' +
127 |       '    " Lastname: " + lname;'
128 |     ).toReturn(
129 |       'var foo =\n' +
130 |       '    // First comment\n' +
131 |       '    // Second comment\n' +
132 |       '    // Third comment\n' +
133 |       '    `Firstname: ${fname} Middlename: ${mname} Lastname: ${lname}`;'
134 |     );
135 |   });
136 | 
137 |   it('should transform nested concatenation', () => {
138 |     expectTransform(
139 |       '"" + (() => "a" + 2)'
140 |     ).toReturn(
141 |       '`${() => `a${2}`}`'
142 |     );
143 |   });
144 | });
145 | 


--------------------------------------------------------------------------------
/test/transform/whitespaceTest.js:
--------------------------------------------------------------------------------
  1 | import createTestHelpers from '../createTestHelpers';
  2 | const {expectTransform} = createTestHelpers([
  3 |   'class',
  4 |   'template',
  5 |   'arrow',
  6 |   'let',
  7 |   'default-param',
  8 |   'arg-spread',
  9 |   'obj-method',
 10 |   'obj-shorthand',
 11 |   'no-strict',
 12 |   'commonjs',
 13 |   'exponent',
 14 | ]);
 15 | 
 16 | describe('Whitespace', () => {
 17 |   it('should not eliminate leading newlines', () => {
 18 |     expectTransform(
 19 |       '\n\nvar x = 42;'
 20 |     ).toReturn(
 21 |       '\n\nconst x = 42;'
 22 |     );
 23 |   });
 24 | 
 25 |   it('should not eliminate trailing newlines', () => {
 26 |     expectTransform(
 27 |       'var x = 42;\n\n'
 28 |     ).toReturn(
 29 |       'const x = 42;\n\n'
 30 |     );
 31 |   });
 32 | 
 33 |   it('ignores #! comment at the beginning of file', () => {
 34 |     expectTransform(
 35 |       '#!/usr/bin/env node\n' +
 36 |       'var x = 42;'
 37 |     ).toReturn(
 38 |       '#!/usr/bin/env node\n' +
 39 |       'const x = 42;'
 40 |     );
 41 |   });
 42 | 
 43 |   it('ignores #! comment almost at the beginning of file', () => {
 44 |     expectTransform(
 45 |       '\n' +
 46 |       '#!/usr/local/bin/node\n' +
 47 |       'if (true) {\n' +
 48 |       '  var foo = 42;\n' +
 49 |       '}'
 50 |     ).toReturn(
 51 |       '\n' +
 52 |       '#!/usr/local/bin/node\n' +
 53 |       'if (true) {\n' +
 54 |       '  const foo = 42;\n' +
 55 |       '}'
 56 |     );
 57 |   });
 58 | 
 59 |   it('should preserve #! comment using CRLF', () => {
 60 |     expectTransform(
 61 |       '#!/usr/bin/env node\r\n' +
 62 |       'var x = 42;'
 63 |     ).toReturn(
 64 |       '#!/usr/bin/env node\r\n' +
 65 |       'const x = 42;'
 66 |     );
 67 |   });
 68 | 
 69 |   it('should preserve CRLF line terminators', () => {
 70 |     expectTransform(
 71 |       'var f = function(x) {\r\n' +
 72 |       '  if (x > 10)\r\n' +
 73 |       '    return 42;\r\n' +
 74 |       '};'
 75 |     ).toReturn(
 76 |       'const f = x => {\r\n' +
 77 |       '  if (x > 10)\r\n' +
 78 |       '    return 42;\r\n' +
 79 |       '};'
 80 |     );
 81 |   });
 82 | 
 83 |   it('should use LF in case of mixed line terminators', () => {
 84 |     expectTransform(
 85 |       'var f = function(x) {\n' +
 86 |       '  if (x > 10)\r\n' +
 87 |       '    return 42;\r\n' +
 88 |       '};'
 89 |     ).toReturn(
 90 |       'const f = x => {\n' +
 91 |       '  if (x > 10)\n' +
 92 |       '    return 42;\n' +
 93 |       '};'
 94 |     );
 95 |   });
 96 | 
 97 |   it('should preserve TABs', () => {
 98 |     expectTransform(
 99 |       'var f = function(x) {\n' +
100 |       '\tif (x > 10) {\n' +
101 |       '\t\tvar y = 42;\n' +
102 |       '\t\treturn {x: x, y: y};\n' +
103 |       '\t}\n' +
104 |       '};'
105 |     ).toReturn(
106 |       'const f = x => {\n' +
107 |       '\tif (x > 10) {\n' +
108 |       '\t\tconst y = 42;\n' +
109 |       '\t\treturn {x, y};\n' +
110 |       '\t}\n' +
111 |       '};'
112 |     );
113 |   });
114 | });
115 | 


--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
 1 | export type transformTypes =
 2 |     | "class"
 3 |     | "template"
 4 |     | "arrow"
 5 |     | "arrow-return"
 6 |     | "let"
 7 |     | "default-param"
 8 |     | "destruct-param"
 9 |     | "arg-spread"
10 |     | "arg-rest"
11 |     | "obj-method"
12 |     | "obj-shorthand"
13 |     | "no-strict"
14 |     | "commonjs"
15 |     | "exponent"
16 |     | "multi-var"
17 |     | "for-of"
18 |     | "for-each"
19 |     | "includes";
20 | 
21 | export function transform(code: string, transformNames: transformTypes[]): any;


--------------------------------------------------------------------------------