├── .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; --------------------------------------------------------------------------------