├── .gitignore ├── BUILD.md ├── CHANGES.md ├── LICENSE ├── README.md ├── build ├── build.sh ├── bundle.sh ├── clean.sh ├── defs ├── index.html ├── inline-version.js └── prepare.sh ├── defs-cmd.js ├── defs-config.json ├── defs-harmony ├── defs-main.js ├── error.js ├── jshint_globals ├── LICENSE.jshint ├── README └── vars.js ├── loop-closures.md ├── options.js ├── other ├── v8-bug.js ├── v8-for-in-scope-2.js └── v8-for-in-scope.js ├── package.json ├── run-tests.js ├── scope.js ├── semantic-differences.md ├── stats.js └── tests ├── a-out.js ├── a.js ├── allowed-loop-closures-out.js ├── allowed-loop-closures.js ├── catch-out.js ├── catch.js ├── catch2-out.js ├── catch2.js ├── const-assign-stderr ├── const-assign.js ├── duplicate-var-stderr ├── duplicate-var.js ├── early-out.js ├── early.js ├── forbidden-loop-closure-stderr ├── forbidden-loop-closure.js ├── global-name-exists-out.js ├── global-name-exists.js ├── let-already-declared-stderr ├── let-already-declared.js ├── letletlet-out.js ├── letletlet.js ├── named-function-expression-conservative-error-stderr ├── named-function-expression-conservative-error.js ├── named-function-expression-out.js ├── named-function-expression.js ├── rename-array-index-out.js ├── rename-array-index.js ├── rename-out.js ├── rename.js ├── use-before-definition-stderr ├── use-before-definition.js ├── used-in-same-declaration-stderr ├── used-in-same-declaration.js ├── var-inside-let-stderr ├── var-inside-let.js ├── var-let-same-scope-stderr ├── var-let-same-scope.js ├── xdollarzero-out.js └── xdollarzero.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Build instructions 2 | defs is written in constlet style itself. There is an optional build step 3 | where it transpiles itself so that it can execute without the `--harmony` 4 | flag passed to node. There's another where Browserify bundles it up with 5 | its dependencies in a single JS file so that it can run in a browser. 6 | 7 | The git repository contains the original constlet style source code as well 8 | as the build scripts. It does not contain build artefacts (transpiled or 9 | bundled source). 10 | 11 | The build scripts populates the `build/es5` and `build/browser` directories. 12 | The NPM package contains a snapshot of the git repository at the time as 13 | well as `build/es5`. `package.json` refers to the transpiled version in 14 | `build/es5`, so there's no need to execute node with `--harmony` when 15 | running a `npm -g` installed `defs` from the command line or when doing a 16 | `require("defs")` of the same. 17 | 18 | If you clone the git repository then don't forget to also `npm install` the 19 | dependencies (see `package.json`). 20 | 21 | If you want to run defs in its original form (rather than transpiled), for 22 | instance if you're hacking on it, then just run the tool via `defs-harmony` 23 | (not a NPM exported binary but check the package root) or include it as a 24 | library via `require("defs.js/defs-main")`. This applies to a git 25 | clone just as well as the NPM package. 26 | 27 | `run-tests.js` is the test runner. It executes a fresh node/defs process 28 | for every test case. Run it on the original source via 29 | `node --harmony run-tests.js` - meaning the test-runner is executed in 30 | `--harmony` mode (because the runner is constlet style) and the child 31 | processes are too (because defs is constlet style). Run it on the 32 | transpiled source (i.e. `build/es5`) via `node run-tests.js es5` - meaning 33 | the test-runner and the child processes are executed in regular es5 (all 34 | have been transpiled). The tests are run automatically in the build scripts. 35 | 36 | To build, `cd build` then run `./build.sh` for self transpilation and 37 | `./bundle.sh` to create a (self transpiled) browser bundle using Browserify. 38 | Open up `build/browser/index.html` in your favorite browser to test the 39 | latter. `./clean.sh` removes the build artefacts. 40 | 41 | I use `prepare.sh` to prepare a release tarball for NPM publishing. 42 | 43 | Happy hacking! 44 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## v1.1.1 2015-10-09 2 | * defs is done 3 | 4 | ## v1.1.0 2014-11-28 5 | * Use esprima-fb instead of upstream Esprima harmony branch 6 | 7 | ## v1.0.1 2014-10-09 8 | * Bump yargs dependency to get rid of transitive ^ dependencies 9 | 10 | ## v1.0.0 2014-07-18 11 | * Use bleeding edge Esprima (harmony branch) 12 | * Support ForOfStatement (issue #24) 13 | * Added --config switch 14 | 15 | ## v0.6.2 2013-12-09 16 | * Fix regenerator breakage due to input handling (pr #22) 17 | 18 | ## v0.6.1 2013-12-09 19 | * Graceful error handling on malformed source input (issue #18) 20 | 21 | ## v0.6.0 2013-12-01 22 | * Accept a custom parse function 23 | * Accept AST input when used as a library 24 | * Bugfix AST modification (issue #19) 25 | 26 | ## v0.5.0 2013-09-30 27 | * Loop closure IIFE transformation support 28 | * Search for defs-config.json upwards in filesystem 29 | * Improved error messages 30 | 31 | ## v0.4.3 2013-09-05 32 | * Improved loop closure detection as to remove false positives 33 | * Improved wrapper shell script (symlink handling) 34 | 35 | ## v0.4.2 2013-09-01 36 | * Improved wrapper script and runner for Linux compat 37 | * breakout module ast-traverse 38 | 39 | ## v0.4.1 2013-07-28 40 | * Bugfix named function expressions (issue #12) 41 | 42 | ## v0.4 2013-07-10 43 | * defs became self aware 44 | * NPM package includes (and defaults to) the self-transpiled version 45 | * Bugfix renaming of index-expressions such as `arr[i]` (issue #10) 46 | 47 | ## v0.3 2013-07-05 48 | * defs used as a library returns errors collected to `ret.errors` instead 49 | of writing to stderr. This also deprecates `ret.exitcode` 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Olov Lassus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SO LONG AND THANKS FOR ALL THE BITS 2 | **defs is done. I recommend migrating to the TypeScript `tsc` compiler because 3 | it does what defs does as good or better, and it does much more.** 4 | 5 | 6 | # defs.js 7 | Static scope analysis and transpilation of ES6 block scoped `const` and `let` 8 | variables, to ES3. 9 | 10 | Node already supports `const` and `let` so you can use that today 11 | (run `node --harmony` and `"use strict"`). `defs.js` enables you to do the same 12 | for browser code. While developing you can rely on the experimental support 13 | in Chrome (chrome://flags, check Enable experimental JavaScript). `defs.js` is 14 | also a pretty decent static scope analyzer/linter. 15 | 16 | The talk 17 | [LET's CONST together, right now (with ES3)](http://vimeo.com/66501924) 18 | from Front-Trends 2013 19 | ([slides](http://blog.lassus.se/files/lets_const_together_ft2013.pdf)) includes 20 | more information about `let`, `const` and `defs.js`. See also the blog post 21 | [ES3 <3 block scoped const and let => defs.js](http://blog.lassus.se/2013/05/defsjs.html). 22 | 23 | 24 | ## Installation and usage 25 | npm install -g defs 26 | 27 | Then run it as `defs file.js`. The errors (if any) will go to stderr, 28 | the transpiled source to `stdout`, so redirect it like `defs file.js > output.js`. 29 | More command line options are coming. 30 | 31 | There's also a [Grunt](http://gruntjs.com/) plugin, see [grunt-defs](https://npmjs.org/package/grunt-defs). 32 | 33 | See [BUILD.md](BUILD.md) for a description of the self-build and the browser bundle. 34 | 35 | ## License 36 | `MIT`, see [LICENSE](LICENSE) file. 37 | 38 | 39 | ## Changes 40 | See [CHANGES.md](CHANGES.md). 41 | 42 | 43 | ## Configuration 44 | `defs` looks for a `defs-config.json` configuration file in your current 45 | directory. If not found there, it searches parent directories until it hits `/`. 46 | You may instead pass a custom `defs-config.json` using `--config`, i.e. 47 | `defs --config path/to/defs-config.json file.js > output.js`. 48 | 49 | Example `defs-config.json`: 50 | 51 | { 52 | "environments": ["node", "browser"], 53 | 54 | "globals": { 55 | "my": false, 56 | "hat": true 57 | }, 58 | "loopClosures": "iife", 59 | "disallowVars": false, 60 | "disallowDuplicated": true, 61 | "disallowUnknownReferences": true 62 | } 63 | 64 | `globals` lets you list your program's globals, and indicate whether they are 65 | writable (`true`) or read-only (`false`), just like `jshint`. 66 | 67 | `environments` lets you import a set of pre-defined globals, here `node` and 68 | `browser`. These default environments are borrowed from `jshint` (see 69 | [jshint_globals/vars.js](https://github.com/olov/defs/blob/master/jshint_globals/vars.js)). 70 | 71 | `loopClosures` (defaults to `false`) can be set to "iife" to enable transformation 72 | of loop-closures via immediately-invoked function expressions. 73 | 74 | `disallowVars` (defaults to `false`) can be enabled to make 75 | usage of `var` an error. 76 | 77 | `disallowDuplicated` (defaults to `true`) errors on duplicated 78 | `var` definitions in the same function scope. 79 | 80 | `disallowUnknownReferences` (defaults to `true`) errors on references to 81 | unknown global variables. 82 | 83 | `ast` (defaults to `false`) produces an AST instead of source code 84 | (experimental). 85 | 86 | `stats` (defaults to `false`) prints const/let statistics and renames 87 | (experimental). 88 | 89 | `parse` (defaults to `null`) lets you provide a custom parse function if you 90 | use defs as an API. By default it will use `require("esprima").parse`. 91 | 92 | 93 | ## Example 94 | 95 | Input `example.js`: 96 | 97 | ```javascript 98 | "use strict"; 99 | function fn() { 100 | const y = 0; 101 | for (let x = 0; x < 10; x++) { 102 | const y = x * 2; 103 | const z = y; 104 | } 105 | console.log(y); // prints 0 106 | } 107 | fn(); 108 | ``` 109 | 110 | Output from running `defs example.js`: 111 | 112 | ```javascript 113 | "use strict"; 114 | function fn() { 115 | var y = 0; 116 | for (var x = 0; x < 10; x++) { 117 | var y$0 = x * 2; 118 | var z = y$0; 119 | } 120 | console.log(y); // prints 0 121 | } 122 | fn(); 123 | ``` 124 | 125 | 126 | ## defs.js used as a library 127 | `npm install defs`, then: 128 | 129 | ```javascript 130 | const defs = require("defs"); 131 | const options = {}; 132 | const src = "const x = 1"; 133 | const res = defs(src, options); 134 | assert(res.src === "var x = 1"); 135 | 136 | // you can also pass an AST (with loc and range) instead of a string to defs 137 | const ast = require("esprima").parse(src, {loc: true, range: true}); 138 | const res = defs(ast, {ast: true}); // AST-in, AST-out 139 | // inspect res.ast 140 | ``` 141 | 142 | res object: 143 | 144 | { 145 | src: string // on success 146 | errors: array of error messages // on errors 147 | stats: statistics object (toStringable) 148 | ast: transformed ast // when options.ast is set 149 | } 150 | 151 | 152 | ## Compatibility 153 | `defs.js` strives to transpile your program as true to ES6 block scope semantics as 154 | possible while being as maximally non-intrusive as possible. 155 | 156 | It can optionally transform loop closures via IIFE's (when possible), if you include 157 | `"loopClosures": "iife"` in your `defs-config.json`. More info in 158 | [loop-closures.md](loop-closures.md). 159 | 160 | See [semantic-differences.md](semantic-differences.md) for other minor differences. 161 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "beginning defs self-build" 3 | rm -rf es5 4 | mkdir es5 5 | 6 | declare -a files=(defs-main.js defs-cmd.js error.js options.js run-tests.js scope.js stats.js) 7 | for i in ${files[@]} 8 | do 9 | echo "building $i" 10 | node --harmony ../defs-cmd ../$i > es5/$i 11 | done 12 | 13 | cp defs es5/ 14 | 15 | echo "hard-coding version" 16 | node --harmony inline-version.js 17 | 18 | cp -r ../jshint_globals es5/ 19 | 20 | cd es5 21 | 22 | echo "running tests (in es5 mode i.e. without --harmony)" 23 | /usr/bin/env node run-tests.js es5 24 | echo "done self-build" 25 | -------------------------------------------------------------------------------- /build/bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | type browserify >/dev/null 2>&1 || { 3 | echo >&2 "error: browserify not found in path" 4 | echo >&2 " try npm install -g browserify"; exit 1; 5 | } 6 | echo "building before creating bundle" 7 | ./build.sh 8 | echo "creating defs_bundle.js via browserify" 9 | rm -rf browser 10 | mkdir browser 11 | cp index.html browser/ 12 | cd es5 13 | browserify -r "./defs-main" > ../browser/defs_bundle.js 14 | echo "done bundle" 15 | -------------------------------------------------------------------------------- /build/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "cleaning build files" 3 | rm -rf es5 browser npm 4 | echo "done cleaning" 5 | -------------------------------------------------------------------------------- /build/defs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("./defs-cmd"); 3 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Run bundle.sh, then bring up your JavaScript console and call defs 5 |

6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /build/inline-version.js: -------------------------------------------------------------------------------- 1 | const version = require("../package.json").version 2 | const fs = require("fs"); 3 | 4 | const src = String(fs.readFileSync("es5/defs-cmd.js")); 5 | const dst = src.replace('require("./package.json").version', JSON.stringify(version)); 6 | fs.writeFileSync("es5/defs-cmd.js", dst); 7 | -------------------------------------------------------------------------------- /build/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd .. 3 | rm -rf build/npm 4 | mkdir build/npm 5 | git archive master -o build/npm/defs.tar --prefix=defs/ 6 | cd build/npm 7 | tar xf defs.tar && rm defs.tar 8 | cd defs/build 9 | ./build.sh 10 | cd ../.. 11 | tar czf defs.tgz defs && rm -rf defs 12 | -------------------------------------------------------------------------------- /defs-cmd.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const fmt = require("simple-fmt"); 5 | const tryor = require("tryor"); 6 | const defs = require("./defs-main"); 7 | const version = require("./package.json").version; 8 | const yargs = require("yargs") 9 | .options("config", {}); 10 | const argv = yargs.argv; 11 | 12 | if (!argv._.length) { 13 | const usage = [ 14 | "defs v" + version + "", 15 | "", 16 | "Usage: defs OPTIONS ", 17 | "", 18 | "Options: ", 19 | " --config use specified defs-config.js instead of searching for it", 20 | ].join("\n"); 21 | console.error(usage); 22 | process.exit(-1); 23 | } 24 | const filename = argv._[0]; 25 | 26 | if (!fs.existsSync(filename)) { 27 | console.error("error: file not found <%s>", filename); 28 | process.exit(-1); 29 | } 30 | 31 | const src = String(fs.readFileSync(filename)); 32 | 33 | const config = findAndReadConfig(); 34 | 35 | const ret = defs(src, config); 36 | if (ret.errors) { 37 | process.stderr.write(ret.errors.join("\n")); 38 | process.stderr.write("\n"); 39 | process.exit(-1); 40 | } 41 | 42 | if (config.stats) { 43 | process.stdout.write(ret.stats.toString()); 44 | process.exit(0); 45 | } 46 | if (ret.ast) { 47 | process.stdout.write(JSON.stringify(ret.ast, null, 4)); 48 | } 49 | if (ret.src) { 50 | process.stdout.write(ret.src); 51 | } 52 | 53 | function findAndReadConfig() { 54 | if (argv.config) { 55 | const config = tryor(function() { return String(fs.readFileSync(argv.config)) }, null); 56 | if (!config) { 57 | console.error("error: config file not found <%s>", argv.config); 58 | process.exit(-1); 59 | } 60 | const json = tryor(function() { return JSON.parse(config) }, null); 61 | if (!json) { 62 | console.error("error: config file is not valid JSON <%s>", argv.config); 63 | process.exit(-1); 64 | } 65 | return json; 66 | } 67 | 68 | let path = ""; 69 | let filename = "defs-config.json"; 70 | let filenamePath = null; 71 | 72 | while (fs.existsSync(path || ".")) { 73 | filenamePath = path + filename; 74 | if (fs.existsSync(filenamePath)) { 75 | const config = tryor(function() { 76 | return JSON.parse(String(fs.readFileSync(filenamePath))); 77 | }, null); 78 | if (config === null) { 79 | console.error("error: config file is not valid JSON <%s>", filenamePath); 80 | process.exit(-1); 81 | } 82 | return config; 83 | } 84 | 85 | path = "../" + path; 86 | } 87 | 88 | return {}; 89 | } -------------------------------------------------------------------------------- /defs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": ["node"], 3 | "disallowDuplicated": true, 4 | "disallowUnknownReferences": true, 5 | "loopClosures": "iife", 6 | "ast": false, 7 | "stats": false 8 | } 9 | -------------------------------------------------------------------------------- /defs-harmony: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rdlkf() { [ -L "$1" ] && (local lk="$(readlink "$1")"; local d="$(dirname "$1")"; cd "$d"; local l="$(rdlkf "$lk")"; ([[ "$l" = /* ]] && echo "$l" || echo "$d/$l")) || echo "$1"; } 3 | DIR="$(dirname "$(rdlkf "$0")")" 4 | /usr/bin/env node --harmony "$DIR/defs-cmd.js" "$@" 5 | -------------------------------------------------------------------------------- /defs-main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"); 4 | const is = require("simple-is"); 5 | const fmt = require("simple-fmt"); 6 | const stringmap = require("stringmap"); 7 | const stringset = require("stringset"); 8 | const alter = require("alter"); 9 | const traverse = require("ast-traverse"); 10 | const breakable = require("breakable"); 11 | const Scope = require("./scope"); 12 | const error = require("./error"); 13 | const getline = error.getline; 14 | const options = require("./options"); 15 | const Stats = require("./stats"); 16 | const jshint_vars = require("./jshint_globals/vars.js"); 17 | 18 | 19 | function isConstLet(kind) { 20 | return is.someof(kind, ["const", "let"]); 21 | } 22 | 23 | function isVarConstLet(kind) { 24 | return is.someof(kind, ["var", "const", "let"]); 25 | } 26 | 27 | function isNonFunctionBlock(node) { 28 | return node.type === "BlockStatement" && is.noneof(node.$parent.type, ["FunctionDeclaration", "FunctionExpression"]); 29 | } 30 | 31 | function isForWithConstLet(node) { 32 | return node.type === "ForStatement" && node.init && node.init.type === "VariableDeclaration" && isConstLet(node.init.kind); 33 | } 34 | 35 | function isForInOfWithConstLet(node) { 36 | return isForInOf(node) && node.left.type === "VariableDeclaration" && isConstLet(node.left.kind); 37 | } 38 | 39 | function isForInOf(node) { 40 | return is.someof(node.type, ["ForInStatement", "ForOfStatement"]); 41 | } 42 | 43 | function isFunction(node) { 44 | return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]); 45 | } 46 | 47 | function isLoop(node) { 48 | return is.someof(node.type, ["ForStatement", "ForInStatement", "ForOfStatement", "WhileStatement", "DoWhileStatement"]); 49 | } 50 | 51 | function isReference(node) { 52 | const parent = node.$parent; 53 | return node.$refToScope || 54 | node.type === "Identifier" && 55 | !(parent.type === "VariableDeclarator" && parent.id === node) && // var|let|const $ 56 | !(parent.type === "MemberExpression" && parent.computed === false && parent.property === node) && // obj.$ 57 | !(parent.type === "Property" && parent.key === node) && // {$: ...} 58 | !(parent.type === "LabeledStatement" && parent.label === node) && // $: ... 59 | !(parent.type === "CatchClause" && parent.param === node) && // catch($) 60 | !(isFunction(parent) && parent.id === node) && // function $(.. 61 | !(isFunction(parent) && is.someof(node, parent.params)) && // function f($).. 62 | true; 63 | } 64 | 65 | function isLvalue(node) { 66 | return isReference(node) && 67 | ((node.$parent.type === "AssignmentExpression" && node.$parent.left === node) || 68 | (node.$parent.type === "UpdateExpression" && node.$parent.argument === node)); 69 | } 70 | 71 | function createScopes(node, parent) { 72 | assert(!node.$scope); 73 | 74 | node.$parent = parent; 75 | node.$scope = node.$parent ? node.$parent.$scope : null; // may be overridden 76 | 77 | if (node.type === "Program") { 78 | // Top-level program is a scope 79 | // There's no block-scope under it 80 | node.$scope = new Scope({ 81 | kind: "hoist", 82 | node: node, 83 | parent: null, 84 | }); 85 | 86 | } else if (isFunction(node)) { 87 | // Function is a scope, with params in it 88 | // There's no block-scope under it 89 | 90 | node.$scope = new Scope({ 91 | kind: "hoist", 92 | node: node, 93 | parent: node.$parent.$scope, 94 | }); 95 | 96 | // function has a name 97 | if (node.id) { 98 | assert(node.id.type === "Identifier"); 99 | 100 | if (node.type === "FunctionDeclaration") { 101 | // Function name goes in parent scope for declared functions 102 | node.$parent.$scope.add(node.id.name, "fun", node.id, null); 103 | } else if (node.type === "FunctionExpression") { 104 | // Function name goes in function's scope for named function expressions 105 | node.$scope.add(node.id.name, "fun", node.id, null); 106 | } else { 107 | assert(false); 108 | } 109 | } 110 | 111 | node.params.forEach(function(param) { 112 | node.$scope.add(param.name, "param", param, null); 113 | }); 114 | 115 | } else if (node.type === "VariableDeclaration") { 116 | // Variable declarations names goes in current scope 117 | assert(isVarConstLet(node.kind)); 118 | node.declarations.forEach(function(declarator) { 119 | assert(declarator.type === "VariableDeclarator"); 120 | const name = declarator.id.name; 121 | if (options.disallowVars && node.kind === "var") { 122 | error(getline(declarator), "var {0} is not allowed (use let or const)", name); 123 | } 124 | node.$scope.add(name, node.kind, declarator.id, declarator.range[1]); 125 | }); 126 | 127 | } else if (isForWithConstLet(node) || isForInOfWithConstLet(node)) { 128 | // For(In/Of) loop with const|let declaration is a scope, with declaration in it 129 | // There may be a block-scope under it 130 | node.$scope = new Scope({ 131 | kind: "block", 132 | node: node, 133 | parent: node.$parent.$scope, 134 | }); 135 | 136 | } else if (isNonFunctionBlock(node)) { 137 | // A block node is a scope unless parent is a function 138 | node.$scope = new Scope({ 139 | kind: "block", 140 | node: node, 141 | parent: node.$parent.$scope, 142 | }); 143 | 144 | } else if (node.type === "CatchClause") { 145 | const identifier = node.param; 146 | 147 | node.$scope = new Scope({ 148 | kind: "catch-block", 149 | node: node, 150 | parent: node.$parent.$scope, 151 | }); 152 | node.$scope.add(identifier.name, "caught", identifier, null); 153 | 154 | // All hoist-scope keeps track of which variables that are propagated through, 155 | // i.e. an reference inside the scope points to a declaration outside the scope. 156 | // This is used to mark "taint" the name since adding a new variable in the scope, 157 | // with a propagated name, would change the meaning of the existing references. 158 | // 159 | // catch(e) is special because even though e is a variable in its own scope, 160 | // we want to make sure that catch(e){let e} is never transformed to 161 | // catch(e){var e} (but rather var e$0). For that reason we taint the use of e 162 | // in the closest hoist-scope, i.e. where var e$0 belongs. 163 | node.$scope.closestHoistScope().markPropagates(identifier.name); 164 | } 165 | } 166 | 167 | function createTopScope(programScope, environments, globals) { 168 | function inject(obj) { 169 | for (let name in obj) { 170 | const writeable = obj[name]; 171 | const kind = (writeable ? "var" : "const"); 172 | if (topScope.hasOwn(name)) { 173 | topScope.remove(name); 174 | } 175 | topScope.add(name, kind, {loc: {start: {line: -1}}}, -1); 176 | } 177 | } 178 | 179 | const topScope = new Scope({ 180 | kind: "hoist", 181 | node: {}, 182 | parent: null, 183 | }); 184 | 185 | const complementary = { 186 | undefined: false, 187 | Infinity: false, 188 | console: false, 189 | }; 190 | 191 | inject(complementary); 192 | inject(jshint_vars.reservedVars); 193 | inject(jshint_vars.ecmaIdentifiers); 194 | if (environments) { 195 | environments.forEach(function(env) { 196 | if (!jshint_vars[env]) { 197 | error(-1, 'environment "{0}" not found', env); 198 | } else { 199 | inject(jshint_vars[env]); 200 | } 201 | }); 202 | } 203 | if (globals) { 204 | inject(globals); 205 | } 206 | 207 | // link it in 208 | programScope.parent = topScope; 209 | topScope.children.push(programScope); 210 | 211 | return topScope; 212 | } 213 | 214 | function setupReferences(ast, allIdentifiers, opts) { 215 | const analyze = (is.own(opts, "analyze") ? opts.analyze : true); 216 | 217 | function visit(node) { 218 | if (!isReference(node)) { 219 | return; 220 | } 221 | allIdentifiers.add(node.name); 222 | 223 | const scope = node.$scope.lookup(node.name); 224 | if (analyze && !scope && options.disallowUnknownReferences) { 225 | error(getline(node), "reference to unknown global variable {0}", node.name); 226 | } 227 | // check const and let for referenced-before-declaration 228 | if (analyze && scope && is.someof(scope.getKind(node.name), ["const", "let"])) { 229 | const allowedFromPos = scope.getFromPos(node.name); 230 | const referencedAtPos = node.range[0]; 231 | assert(is.finitenumber(allowedFromPos)); 232 | assert(is.finitenumber(referencedAtPos)); 233 | if (referencedAtPos < allowedFromPos) { 234 | if (!node.$scope.hasFunctionScopeBetween(scope)) { 235 | error(getline(node), "{0} is referenced before its declaration", node.name); 236 | } 237 | } 238 | } 239 | node.$refToScope = scope; 240 | } 241 | 242 | traverse(ast, {pre: visit}); 243 | } 244 | 245 | // TODO for loops init and body props are parallel to each other but init scope is outer that of body 246 | // TODO is this a problem? 247 | 248 | function varify(ast, stats, allIdentifiers, changes) { 249 | function unique(name) { 250 | assert(allIdentifiers.has(name)); 251 | for (let cnt = 0; ; cnt++) { 252 | const genName = name + "$" + String(cnt); 253 | if (!allIdentifiers.has(genName)) { 254 | return genName; 255 | } 256 | } 257 | } 258 | 259 | function renameDeclarations(node) { 260 | if (node.type === "VariableDeclaration" && isConstLet(node.kind)) { 261 | const hoistScope = node.$scope.closestHoistScope(); 262 | const origScope = node.$scope; 263 | 264 | // text change const|let => var 265 | changes.push({ 266 | start: node.range[0], 267 | end: node.range[0] + node.kind.length, 268 | str: "var", 269 | }); 270 | 271 | node.declarations.forEach(function(declarator) { 272 | assert(declarator.type === "VariableDeclarator"); 273 | const name = declarator.id.name; 274 | 275 | stats.declarator(node.kind); 276 | 277 | // rename if 278 | // 1) name already exists in hoistScope, or 279 | // 2) name is already propagated (passed) through hoistScope or manually tainted 280 | const rename = (origScope !== hoistScope && 281 | (hoistScope.hasOwn(name) || hoistScope.doesPropagate(name))); 282 | 283 | const newName = (rename ? unique(name) : name); 284 | 285 | origScope.remove(name); 286 | hoistScope.add(newName, "var", declarator.id, declarator.range[1]); 287 | 288 | origScope.moves = origScope.moves || stringmap(); 289 | origScope.moves.set(name, { 290 | name: newName, 291 | scope: hoistScope, 292 | }); 293 | 294 | allIdentifiers.add(newName); 295 | 296 | if (newName !== name) { 297 | stats.rename(name, newName, getline(declarator)); 298 | 299 | declarator.id.originalName = name; 300 | declarator.id.name = newName; 301 | 302 | // textchange var x => var x$1 303 | changes.push({ 304 | start: declarator.id.range[0], 305 | end: declarator.id.range[1], 306 | str: newName, 307 | }); 308 | } 309 | }); 310 | 311 | // ast change const|let => var 312 | node.kind = "var"; 313 | } 314 | } 315 | 316 | function renameReferences(node) { 317 | if (!node.$refToScope) { 318 | return; 319 | } 320 | const move = node.$refToScope.moves && node.$refToScope.moves.get(node.name); 321 | if (!move) { 322 | return; 323 | } 324 | node.$refToScope = move.scope; 325 | 326 | if (node.name !== move.name) { 327 | node.originalName = node.name; 328 | node.name = move.name; 329 | 330 | if (node.alterop) { 331 | // node has no range because it is the result of another alter operation 332 | let existingOp = null; 333 | for (let i = 0; i < changes.length; i++) { 334 | const op = changes[i]; 335 | if (op.node === node) { 336 | existingOp = op; 337 | break; 338 | } 339 | } 340 | assert(existingOp); 341 | 342 | // modify op 343 | existingOp.str = move.name; 344 | } else { 345 | changes.push({ 346 | start: node.range[0], 347 | end: node.range[1], 348 | str: move.name, 349 | }); 350 | } 351 | } 352 | } 353 | 354 | traverse(ast, {pre: renameDeclarations}); 355 | traverse(ast, {pre: renameReferences}); 356 | ast.$scope.traverse({pre: function(scope) { 357 | delete scope.moves; 358 | }}); 359 | } 360 | 361 | 362 | function detectLoopClosures(ast) { 363 | traverse(ast, {pre: visit}); 364 | 365 | function detectIifyBodyBlockers(body, node) { 366 | return breakable(function(brk) { 367 | traverse(body, {pre: function(n) { 368 | // if we hit an inner function of the loop body, don't traverse further 369 | if (isFunction(n)) { 370 | return false; 371 | } 372 | 373 | let err = true; // reset to false in else-statement below 374 | const msg = "loop-variable {0} is captured by a loop-closure that can't be transformed due to use of {1} at line {2}"; 375 | if (n.type === "BreakStatement") { 376 | error(getline(node), msg, node.name, "break", getline(n)); 377 | } else if (n.type === "ContinueStatement") { 378 | error(getline(node), msg, node.name, "continue", getline(n)); 379 | } else if (n.type === "ReturnStatement") { 380 | error(getline(node), msg, node.name, "return", getline(n)); 381 | } else if (n.type === "YieldExpression") { 382 | error(getline(node), msg, node.name, "yield", getline(n)); 383 | } else if (n.type === "Identifier" && n.name === "arguments") { 384 | error(getline(node), msg, node.name, "arguments", getline(n)); 385 | } else if (n.type === "VariableDeclaration" && n.kind === "var") { 386 | error(getline(node), msg, node.name, "var", getline(n)); 387 | } else { 388 | err = false; 389 | } 390 | if (err) { 391 | brk(true); // break traversal 392 | } 393 | }}); 394 | return false; 395 | }); 396 | } 397 | 398 | function visit(node) { 399 | // forbidden pattern: 400 | // * * * * 401 | var loopNode = null; 402 | if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) { 403 | // traverse nodes up towards root from constlet-def 404 | // if we hit a function (before a loop) - ok! 405 | // if we hit a loop - maybe-ouch 406 | // if we reach root - ok! 407 | for (let n = node.$refToScope.node; ; ) { 408 | if (isFunction(n)) { 409 | // we're ok (function-local) 410 | return; 411 | } else if (isLoop(n)) { 412 | loopNode = n; 413 | // maybe not ok (between loop and function) 414 | break; 415 | } 416 | n = n.$parent; 417 | if (!n) { 418 | // ok (reached root) 419 | return; 420 | } 421 | } 422 | 423 | assert(isLoop(loopNode)); 424 | 425 | // traverse scopes from reference-scope up towards definition-scope 426 | // if we hit a function, ouch! 427 | const defScope = node.$refToScope; 428 | const generateIIFE = (options.loopClosures === "iife"); 429 | 430 | for (let s = node.$scope; s; s = s.parent) { 431 | if (s === defScope) { 432 | // we're ok 433 | return; 434 | } else if (isFunction(s.node)) { 435 | // not ok (there's a function between the reference and definition) 436 | // may be transformable via IIFE 437 | 438 | if (!generateIIFE) { 439 | const msg = "loop-variable {0} is captured by a loop-closure. Tried \"loopClosures\": \"iife\" in defs-config.json?"; 440 | return error(getline(node), msg, node.name); 441 | } 442 | 443 | // here be dragons 444 | // for (let x = ..; .. ; ..) { (function(){x})() } is forbidden because of current 445 | // spec and VM status 446 | if (loopNode.type === "ForStatement" && defScope.node === loopNode) { 447 | const declarationNode = defScope.getNode(node.name); 448 | return error(getline(declarationNode), "Not yet specced ES6 feature. {0} is declared in for-loop header and then captured in loop closure", declarationNode.name); 449 | } 450 | 451 | // speak now or forever hold your peace 452 | if (detectIifyBodyBlockers(loopNode.body, node)) { 453 | // error already generated 454 | return; 455 | } 456 | 457 | // mark loop for IIFE-insertion 458 | loopNode.$iify = true; 459 | } 460 | } 461 | } 462 | } 463 | } 464 | 465 | function transformLoopClosures(root, ops, options) { 466 | function insertOp(pos, str, node) { 467 | const op = { 468 | start: pos, 469 | end: pos, 470 | str: str, 471 | } 472 | if (node) { 473 | op.node = node; 474 | } 475 | ops.push(op); 476 | } 477 | 478 | traverse(root, {pre: function(node) { 479 | if (!node.$iify) { 480 | return; 481 | } 482 | 483 | const hasBlock = (node.body.type === "BlockStatement"); 484 | 485 | const insertHead = (hasBlock ? 486 | node.body.range[0] + 1 : // just after body { 487 | node.body.range[0]); // just before existing expression 488 | const insertFoot = (hasBlock ? 489 | node.body.range[1] - 1 : // just before body } 490 | node.body.range[1]); // just after existing expression 491 | 492 | const forInName = (isForInOf(node) && node.left.declarations[0].id.name);; 493 | const iifeHead = fmt("(function({0}){", forInName ? forInName : ""); 494 | const iifeTail = fmt("}).call(this{0});", forInName ? ", " + forInName : ""); 495 | 496 | // modify AST 497 | const iifeFragment = options.parse(iifeHead + iifeTail); 498 | const iifeExpressionStatement = iifeFragment.body[0]; 499 | const iifeBlockStatement = iifeExpressionStatement.expression.callee.object.body; 500 | 501 | if (hasBlock) { 502 | const forBlockStatement = node.body; 503 | const tmp = forBlockStatement.body; 504 | forBlockStatement.body = [iifeExpressionStatement]; 505 | iifeBlockStatement.body = tmp; 506 | } else { 507 | const tmp = node.body; 508 | node.body = iifeExpressionStatement; 509 | iifeBlockStatement.body[0] = tmp; 510 | } 511 | 512 | // create ops 513 | insertOp(insertHead, iifeHead); 514 | 515 | if (forInName) { 516 | insertOp(insertFoot, "}).call(this, "); 517 | 518 | const args = iifeExpressionStatement.expression.arguments; 519 | const iifeArgumentIdentifier = args[1]; 520 | iifeArgumentIdentifier.alterop = true; 521 | insertOp(insertFoot, forInName, iifeArgumentIdentifier); 522 | 523 | insertOp(insertFoot, ");"); 524 | } else { 525 | insertOp(insertFoot, iifeTail); 526 | } 527 | }}); 528 | } 529 | 530 | function detectConstAssignment(ast) { 531 | traverse(ast, {pre: function(node) { 532 | if (isLvalue(node)) { 533 | const scope = node.$scope.lookup(node.name); 534 | if (scope && scope.getKind(node.name) === "const") { 535 | error(getline(node), "can't assign to const variable {0}", node.name); 536 | } 537 | } 538 | }}); 539 | } 540 | 541 | function detectConstantLets(ast) { 542 | traverse(ast, {pre: function(node) { 543 | if (isLvalue(node)) { 544 | const scope = node.$scope.lookup(node.name); 545 | if (scope) { 546 | scope.markWrite(node.name); 547 | } 548 | } 549 | }}); 550 | 551 | ast.$scope.detectUnmodifiedLets(); 552 | } 553 | 554 | function setupScopeAndReferences(root, opts) { 555 | // setup scopes 556 | traverse(root, {pre: createScopes}); 557 | const topScope = createTopScope(root.$scope, options.environments, options.globals); 558 | 559 | // allIdentifiers contains all declared and referenced vars 560 | // collect all declaration names (including those in topScope) 561 | const allIdentifiers = stringset(); 562 | topScope.traverse({pre: function(scope) { 563 | allIdentifiers.addMany(scope.decls.keys()); 564 | }}); 565 | 566 | // setup node.$refToScope, check for errors. 567 | // also collects all referenced names to allIdentifiers 568 | setupReferences(root, allIdentifiers, opts); 569 | return allIdentifiers; 570 | } 571 | 572 | function cleanupTree(root) { 573 | traverse(root, {pre: function(node) { 574 | for (let prop in node) { 575 | if (prop[0] === "$") { 576 | delete node[prop]; 577 | } 578 | } 579 | }}); 580 | } 581 | 582 | function run(src, config) { 583 | // alter the options singleton with user configuration 584 | for (let key in config) { 585 | options[key] = config[key]; 586 | } 587 | 588 | let parsed; 589 | 590 | if (is.object(src)) { 591 | if (!options.ast) { 592 | return { 593 | errors: [ 594 | "Can't produce string output when input is an AST. " + 595 | "Did you forget to set options.ast = true?" 596 | ], 597 | }; 598 | } 599 | 600 | // Received an AST object as src, so no need to parse it. 601 | parsed = src; 602 | 603 | } else if (is.string(src)) { 604 | try { 605 | parsed = options.parse(src, { 606 | loc: true, 607 | range: true, 608 | }); 609 | } catch (e) { 610 | return { 611 | errors: [ 612 | fmt("line {0} column {1}: Error during input file parsing\n{2}\n{3}", 613 | e.lineNumber, 614 | e.column, 615 | src.split("\n")[e.lineNumber - 1], 616 | fmt.repeat(" ", e.column - 1) + "^") 617 | ], 618 | }; 619 | } 620 | 621 | } else { 622 | return { 623 | errors: ["Input was neither an AST object nor a string."], 624 | }; 625 | } 626 | 627 | const ast = parsed; 628 | 629 | // TODO detect unused variables (never read) 630 | error.reset(); 631 | 632 | let allIdentifiers = setupScopeAndReferences(ast, {}); 633 | 634 | // static analysis passes 635 | detectLoopClosures(ast); 636 | detectConstAssignment(ast); 637 | //detectConstantLets(ast); 638 | 639 | const changes = []; 640 | transformLoopClosures(ast, changes, options); 641 | 642 | //ast.$scope.print(); process.exit(-1); 643 | 644 | if (error.errors.length >= 1) { 645 | return { 646 | errors: error.errors, 647 | }; 648 | } 649 | 650 | if (changes.length > 0) { 651 | cleanupTree(ast); 652 | allIdentifiers = setupScopeAndReferences(ast, {analyze: false}); 653 | } 654 | assert(error.errors.length === 0); 655 | 656 | // change constlet declarations to var, renamed if needed 657 | // varify modifies the scopes and AST accordingly and 658 | // returns a list of change fragments (to use with alter) 659 | const stats = new Stats(); 660 | varify(ast, stats, allIdentifiers, changes); 661 | 662 | if (options.ast) { 663 | // return the modified AST instead of src code 664 | // get rid of all added $ properties first, such as $parent and $scope 665 | cleanupTree(ast); 666 | return { 667 | stats: stats, 668 | ast: ast, 669 | }; 670 | } else { 671 | // apply changes produced by varify and return the transformed src 672 | const transformedSrc = alter(src, changes); 673 | return { 674 | stats: stats, 675 | src: transformedSrc, 676 | }; 677 | } 678 | } 679 | 680 | module.exports = run; 681 | -------------------------------------------------------------------------------- /error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fmt = require("simple-fmt"); 4 | const assert = require("assert"); 5 | 6 | function error(line, var_args) { 7 | assert(arguments.length >= 2); 8 | 9 | const msg = (arguments.length === 2 ? 10 | String(var_args) : fmt.apply(fmt, Array.prototype.slice.call(arguments, 1))); 11 | 12 | error.errors.push(line === -1 ? msg : fmt("line {0}: {1}", line, msg)); 13 | } 14 | 15 | error.reset = function() { 16 | error.errors = []; 17 | }; 18 | 19 | error.getline = function(node) { 20 | if (node && node.loc && node.loc.start) { 21 | return node.loc.start.line; 22 | } 23 | return -1; 24 | }; 25 | 26 | error.reset(); 27 | 28 | module.exports = error; 29 | -------------------------------------------------------------------------------- /jshint_globals/LICENSE.jshint: -------------------------------------------------------------------------------- 1 | Copyright 2012 Anton Kovalyov (http://jshint.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /jshint_globals/README: -------------------------------------------------------------------------------- 1 | The file vars.js is copied from the JSHint project (http://jshint.com). See LICENSE.jshint. 2 | -------------------------------------------------------------------------------- /jshint_globals/vars.js: -------------------------------------------------------------------------------- 1 | // jshint -W001 2 | 3 | "use strict"; 4 | 5 | // Identifiers provided by the ECMAScript standard. 6 | 7 | exports.reservedVars = { 8 | arguments : false, 9 | NaN : false 10 | }; 11 | 12 | exports.ecmaIdentifiers = { 13 | Array : false, 14 | Boolean : false, 15 | Date : false, 16 | decodeURI : false, 17 | decodeURIComponent : false, 18 | encodeURI : false, 19 | encodeURIComponent : false, 20 | Error : false, 21 | "eval" : false, 22 | EvalError : false, 23 | Function : false, 24 | hasOwnProperty : false, 25 | isFinite : false, 26 | isNaN : false, 27 | JSON : false, 28 | Math : false, 29 | Map : false, 30 | Number : false, 31 | Object : false, 32 | parseInt : false, 33 | parseFloat : false, 34 | RangeError : false, 35 | ReferenceError : false, 36 | RegExp : false, 37 | Set : false, 38 | String : false, 39 | SyntaxError : false, 40 | TypeError : false, 41 | URIError : false, 42 | WeakMap : false 43 | }; 44 | 45 | // Global variables commonly provided by a web browser environment. 46 | 47 | exports.browser = { 48 | ArrayBuffer : false, 49 | ArrayBufferView : false, 50 | Audio : false, 51 | Blob : false, 52 | addEventListener : false, 53 | applicationCache : false, 54 | atob : false, 55 | blur : false, 56 | btoa : false, 57 | clearInterval : false, 58 | clearTimeout : false, 59 | close : false, 60 | closed : false, 61 | DataView : false, 62 | DOMParser : false, 63 | defaultStatus : false, 64 | document : false, 65 | Element : false, 66 | event : false, 67 | FileReader : false, 68 | Float32Array : false, 69 | Float64Array : false, 70 | FormData : false, 71 | focus : false, 72 | frames : false, 73 | getComputedStyle : false, 74 | HTMLElement : false, 75 | HTMLAnchorElement : false, 76 | HTMLBaseElement : false, 77 | HTMLBlockquoteElement: false, 78 | HTMLBodyElement : false, 79 | HTMLBRElement : false, 80 | HTMLButtonElement : false, 81 | HTMLCanvasElement : false, 82 | HTMLDirectoryElement : false, 83 | HTMLDivElement : false, 84 | HTMLDListElement : false, 85 | HTMLFieldSetElement : false, 86 | HTMLFontElement : false, 87 | HTMLFormElement : false, 88 | HTMLFrameElement : false, 89 | HTMLFrameSetElement : false, 90 | HTMLHeadElement : false, 91 | HTMLHeadingElement : false, 92 | HTMLHRElement : false, 93 | HTMLHtmlElement : false, 94 | HTMLIFrameElement : false, 95 | HTMLImageElement : false, 96 | HTMLInputElement : false, 97 | HTMLIsIndexElement : false, 98 | HTMLLabelElement : false, 99 | HTMLLayerElement : false, 100 | HTMLLegendElement : false, 101 | HTMLLIElement : false, 102 | HTMLLinkElement : false, 103 | HTMLMapElement : false, 104 | HTMLMenuElement : false, 105 | HTMLMetaElement : false, 106 | HTMLModElement : false, 107 | HTMLObjectElement : false, 108 | HTMLOListElement : false, 109 | HTMLOptGroupElement : false, 110 | HTMLOptionElement : false, 111 | HTMLParagraphElement : false, 112 | HTMLParamElement : false, 113 | HTMLPreElement : false, 114 | HTMLQuoteElement : false, 115 | HTMLScriptElement : false, 116 | HTMLSelectElement : false, 117 | HTMLStyleElement : false, 118 | HTMLTableCaptionElement: false, 119 | HTMLTableCellElement : false, 120 | HTMLTableColElement : false, 121 | HTMLTableElement : false, 122 | HTMLTableRowElement : false, 123 | HTMLTableSectionElement: false, 124 | HTMLTextAreaElement : false, 125 | HTMLTitleElement : false, 126 | HTMLUListElement : false, 127 | HTMLVideoElement : false, 128 | history : false, 129 | Int16Array : false, 130 | Int32Array : false, 131 | Int8Array : false, 132 | Image : false, 133 | length : false, 134 | localStorage : false, 135 | location : false, 136 | MessageChannel : false, 137 | MessageEvent : false, 138 | MessagePort : false, 139 | moveBy : false, 140 | moveTo : false, 141 | MutationObserver : false, 142 | name : false, 143 | Node : false, 144 | NodeFilter : false, 145 | navigator : false, 146 | onbeforeunload : true, 147 | onblur : true, 148 | onerror : true, 149 | onfocus : true, 150 | onload : true, 151 | onresize : true, 152 | onunload : true, 153 | open : false, 154 | openDatabase : false, 155 | opener : false, 156 | Option : false, 157 | parent : false, 158 | print : false, 159 | removeEventListener : false, 160 | resizeBy : false, 161 | resizeTo : false, 162 | screen : false, 163 | scroll : false, 164 | scrollBy : false, 165 | scrollTo : false, 166 | sessionStorage : false, 167 | setInterval : false, 168 | setTimeout : false, 169 | SharedWorker : false, 170 | status : false, 171 | top : false, 172 | Uint16Array : false, 173 | Uint32Array : false, 174 | Uint8Array : false, 175 | Uint8ClampedArray : false, 176 | WebSocket : false, 177 | window : false, 178 | Worker : false, 179 | XMLHttpRequest : false, 180 | XMLSerializer : false, 181 | XPathEvaluator : false, 182 | XPathException : false, 183 | XPathExpression : false, 184 | XPathNamespace : false, 185 | XPathNSResolver : false, 186 | XPathResult : false 187 | }; 188 | 189 | exports.devel = { 190 | alert : false, 191 | confirm: false, 192 | console: false, 193 | Debug : false, 194 | opera : false, 195 | prompt : false 196 | }; 197 | 198 | exports.worker = { 199 | importScripts: true, 200 | postMessage : true, 201 | self : true 202 | }; 203 | 204 | // Widely adopted global names that are not part of ECMAScript standard 205 | exports.nonstandard = { 206 | escape : false, 207 | unescape: false 208 | }; 209 | 210 | // Globals provided by popular JavaScript environments. 211 | 212 | exports.couch = { 213 | "require" : false, 214 | respond : false, 215 | getRow : false, 216 | emit : false, 217 | send : false, 218 | start : false, 219 | sum : false, 220 | log : false, 221 | exports : false, 222 | module : false, 223 | provides : false 224 | }; 225 | 226 | exports.node = { 227 | __filename : false, 228 | __dirname : false, 229 | Buffer : false, 230 | DataView : false, 231 | console : false, 232 | exports : true, // In Node it is ok to exports = module.exports = foo(); 233 | GLOBAL : false, 234 | global : false, 235 | module : false, 236 | process : false, 237 | require : false, 238 | setTimeout : false, 239 | clearTimeout : false, 240 | setInterval : false, 241 | clearInterval: false 242 | }; 243 | 244 | exports.phantom = { 245 | phantom : true, 246 | require : true, 247 | WebPage : true 248 | }; 249 | 250 | exports.rhino = { 251 | defineClass : false, 252 | deserialize : false, 253 | gc : false, 254 | help : false, 255 | importPackage: false, 256 | "java" : false, 257 | load : false, 258 | loadClass : false, 259 | print : false, 260 | quit : false, 261 | readFile : false, 262 | readUrl : false, 263 | runCommand : false, 264 | seal : false, 265 | serialize : false, 266 | spawn : false, 267 | sync : false, 268 | toint32 : false, 269 | version : false 270 | }; 271 | 272 | exports.wsh = { 273 | ActiveXObject : true, 274 | Enumerator : true, 275 | GetObject : true, 276 | ScriptEngine : true, 277 | ScriptEngineBuildVersion : true, 278 | ScriptEngineMajorVersion : true, 279 | ScriptEngineMinorVersion : true, 280 | VBArray : true, 281 | WSH : true, 282 | WScript : true, 283 | XDomainRequest : true 284 | }; 285 | 286 | // Globals provided by popular JavaScript libraries. 287 | 288 | exports.dojo = { 289 | dojo : false, 290 | dijit : false, 291 | dojox : false, 292 | define : false, 293 | "require": false 294 | }; 295 | 296 | exports.jquery = { 297 | "$" : false, 298 | jQuery : false 299 | }; 300 | 301 | exports.mootools = { 302 | "$" : false, 303 | "$$" : false, 304 | Asset : false, 305 | Browser : false, 306 | Chain : false, 307 | Class : false, 308 | Color : false, 309 | Cookie : false, 310 | Core : false, 311 | Document : false, 312 | DomReady : false, 313 | DOMEvent : false, 314 | DOMReady : false, 315 | Drag : false, 316 | Element : false, 317 | Elements : false, 318 | Event : false, 319 | Events : false, 320 | Fx : false, 321 | Group : false, 322 | Hash : false, 323 | HtmlTable : false, 324 | Iframe : false, 325 | IframeShim : false, 326 | InputValidator: false, 327 | instanceOf : false, 328 | Keyboard : false, 329 | Locale : false, 330 | Mask : false, 331 | MooTools : false, 332 | Native : false, 333 | Options : false, 334 | OverText : false, 335 | Request : false, 336 | Scroller : false, 337 | Slick : false, 338 | Slider : false, 339 | Sortables : false, 340 | Spinner : false, 341 | Swiff : false, 342 | Tips : false, 343 | Type : false, 344 | typeOf : false, 345 | URI : false, 346 | Window : false 347 | }; 348 | 349 | exports.prototypejs = { 350 | "$" : false, 351 | "$$" : false, 352 | "$A" : false, 353 | "$F" : false, 354 | "$H" : false, 355 | "$R" : false, 356 | "$break" : false, 357 | "$continue" : false, 358 | "$w" : false, 359 | Abstract : false, 360 | Ajax : false, 361 | Class : false, 362 | Enumerable : false, 363 | Element : false, 364 | Event : false, 365 | Field : false, 366 | Form : false, 367 | Hash : false, 368 | Insertion : false, 369 | ObjectRange : false, 370 | PeriodicalExecuter: false, 371 | Position : false, 372 | Prototype : false, 373 | Selector : false, 374 | Template : false, 375 | Toggle : false, 376 | Try : false, 377 | Autocompleter : false, 378 | Builder : false, 379 | Control : false, 380 | Draggable : false, 381 | Draggables : false, 382 | Droppables : false, 383 | Effect : false, 384 | Sortable : false, 385 | SortableObserver : false, 386 | Sound : false, 387 | Scriptaculous : false 388 | }; 389 | 390 | exports.yui = { 391 | YUI : false, 392 | Y : false, 393 | YUI_config: false 394 | }; 395 | 396 | -------------------------------------------------------------------------------- /loop-closures.md: -------------------------------------------------------------------------------- 1 | # Loop closures 2 | A loop closure that captures a block scoped loop variable can't be transpiled only with 3 | variable renaming. 4 | 5 | Let's take the following example: 6 | 7 | ```javascript 8 | const arr = []; 9 | for (let x = 0; x < 10; x++) { 10 | let y = x; 11 | arr.push(function() { return y; }); 12 | } 13 | ``` 14 | 15 | defs with default options gives you an error: 16 | 17 | line 4: loop-variable y is captured by a loop-closure. Tried "loopClosures": "iife" in defs-config.json? 18 | 19 | This is because With ES6 semantics `y` is bound fresh per loop iteration, so each closure captures a separate 20 | instance of `y`, unlike if `y` would have been a `var`. 21 | 22 | You can now either choose to rewrite it manually (in usual pre-ES6 style), with an IIFE or 23 | bind or similar. For example: 24 | 25 | ```javascript 26 | for (let x = 0; x < 10; x++) { 27 | const arr = []; 28 | (function(y) { 29 | arr.push(function() { return y; }); 30 | })(x); 31 | } 32 | ``` 33 | 34 | And that runs just fine and defs stops complaining. Alternatively, you can ask defs to 35 | create the IIFE for you by adding `"loopClosures": "iife"` to your `defs-config.json`. 36 | Run on the original example, defs transpiles that without complaining to: 37 | 38 | ```javascript 39 | var arr = []; 40 | for (var x = 0; x < 10; x++) {(function(){ 41 | var y = x; 42 | arr.push(function() { return y; }); 43 | }).call(this);} 44 | ``` 45 | 46 | Not all loop closures can be transpiled into IIFE's. If the loop body (which contains the 47 | loop closure) also contains a `return`, `yield`, `break`, `continue`, `arguments` or `var`, then 48 | defs will detect that and give you an error (because an IIFE would likely change 49 | the loop body semantics). If so either rewrite the loop body so it doesn't use any of these or 50 | insert an IIFE manually (knowing what you're doing). 51 | 52 | defs does not support transforming loops containing loop closures in any other way than 53 | with IIFE's, including try-catch statements. 54 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | // default configuration 2 | 3 | module.exports = { 4 | disallowVars: false, 5 | disallowDuplicated: true, 6 | disallowUnknownReferences: true, 7 | parse: require("esprima-fb").parse, 8 | }; 9 | -------------------------------------------------------------------------------- /other/v8-bug.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // https://code.google.com/p/v8/issues/detail?id=2560 4 | // v8 --harmony should print 0 1 2 (not 3 3 3) when executing: 5 | 6 | var arr = []; 7 | for (let x = 0; x < 3; x++) { 8 | arr.push(function() { 9 | console.log(x); 10 | }); 11 | } 12 | arr.forEach(function(f) { f(); }); 13 | -------------------------------------------------------------------------------- /other/v8-for-in-scope-2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // v8 --harmony correctly prints 9 9 9: 4 | // note that the loop terminates 5 | 6 | var arr = []; 7 | for (let x in [0,1,2]) { 8 | let x = 9; 9 | arr.push(function() { 10 | console.log(x); 11 | }); 12 | } 13 | arr.forEach(function(f) { f(); }); 14 | -------------------------------------------------------------------------------- /other/v8-for-in-scope.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // v8 --harmony correctly prints 0 1 2: 4 | 5 | var arr = []; 6 | for (let x in [0,1,2]) { 7 | arr.push(function() { 8 | console.log(x); 9 | }); 10 | } 11 | arr.forEach(function(f) { f(); }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defs", 3 | "version": "1.1.1", 4 | "description": "Static scope analysis and transpilation of ES6 block scoped const and let variables, to ES3.", 5 | "main": "build/es5/defs-main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/olov/defs.git" 9 | }, 10 | "dependencies": { 11 | "alter": "~0.2.0", 12 | "ast-traverse": "~0.1.1", 13 | "breakable": "~1.0.0", 14 | "esprima-fb": "~15001.1001.0-dev-harmony-fb", 15 | "simple-fmt": "~0.1.0", 16 | "simple-is": "~0.2.0", 17 | "stringmap": "~0.2.2", 18 | "stringset": "~0.2.1", 19 | "tryor": "~0.1.2", 20 | "yargs": "~3.27.0" 21 | }, 22 | "devDependencies": { 23 | "diff": "~2.1.3" 24 | }, 25 | "keywords": [ 26 | "defs", 27 | "scope", 28 | "blockscope", 29 | "block-scope", 30 | "let", 31 | "const", 32 | "var", 33 | "es6", 34 | "transpile", 35 | "transpiler", 36 | "lint", 37 | "linter" 38 | ], 39 | "scripts": { 40 | "test": "node --harmony run-tests" 41 | }, 42 | "bin": "./build/es5/defs", 43 | "author": "Olov Lassus ", 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /run-tests.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const fmt = require("simple-fmt"); 3 | const exec = require("child_process").exec; 4 | const diff = require("diff"); 5 | 6 | function slurp(filename) { 7 | return fs.existsSync(filename) ? String(fs.readFileSync(filename)) : ""; 8 | } 9 | 10 | const pathToTests = (fs.existsSync("tests") ? "tests" : "../../tests"); 11 | 12 | const tests = fs.readdirSync(pathToTests).filter(function(filename) { 13 | return !/-out\.js$/.test(filename) && !/-stderr$/.test(filename); 14 | }); 15 | 16 | function run(test) { 17 | function diffOutput(correct, got, name) { 18 | if (got !== correct) { 19 | const patch = diff.createPatch(name, correct, got); 20 | process.stdout.write(patch); 21 | process.stdout.write("\n\n"); 22 | } 23 | } 24 | 25 | const noSuffix = test.slice(0, -3); 26 | exec(fmt("{0} {1} defs-cmd {2}/{3}", NODE, FLAG, pathToTests, test), function(error, stdout, stderr) { 27 | stderr = stderr || ""; 28 | stdout = stdout || ""; 29 | const expectedStderr = slurp(fmt("{0}/{1}-stderr", pathToTests, noSuffix)); 30 | const expectedStdout = slurp(fmt("{0}/{1}-out.js", pathToTests, noSuffix)); 31 | 32 | const pass = (stderr === expectedStderr && stdout === expectedStdout); 33 | 34 | if (!pass) { 35 | console.log(fmt("FAILED test {0}", test)); 36 | } 37 | diffOutput(expectedStdout, stdout, fmt("{0}-out.js", test)); 38 | diffOutput(expectedStderr, stderr, fmt("{0}-stderr", test)); 39 | }); 40 | } 41 | 42 | const NODE = process.argv[0]; 43 | const FLAG = (process.argv[2] === "es5" ? "" : "--harmony"); 44 | tests.forEach(run); 45 | -------------------------------------------------------------------------------- /scope.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"); 4 | const stringmap = require("stringmap"); 5 | const stringset = require("stringset"); 6 | const is = require("simple-is"); 7 | const fmt = require("simple-fmt"); 8 | const error = require("./error"); 9 | const getline = error.getline; 10 | const options = require("./options"); 11 | 12 | function Scope(args) { 13 | assert(is.someof(args.kind, ["hoist", "block", "catch-block"])); 14 | assert(is.object(args.node)); 15 | assert(args.parent === null || is.object(args.parent)); 16 | 17 | // kind === "hoist": function scopes, program scope, injected globals 18 | // kind === "block": ES6 block scopes 19 | // kind === "catch-block": catch block scopes 20 | this.kind = args.kind; 21 | 22 | // the AST node the block corresponds to 23 | this.node = args.node; 24 | 25 | // parent scope 26 | this.parent = args.parent; 27 | 28 | // children scopes for easier traversal (populated internally) 29 | this.children = []; 30 | 31 | // scope declarations. decls[variable_name] = { 32 | // kind: "fun" for functions, 33 | // "param" for function parameters, 34 | // "caught" for catch parameter 35 | // "var", 36 | // "const", 37 | // "let" 38 | // node: the AST node the declaration corresponds to 39 | // from: source code index from which it is visible at earliest 40 | // (only stored for "const", "let" [and "var"] nodes) 41 | // } 42 | this.decls = stringmap(); 43 | 44 | // names of all declarations within this scope that was ever written 45 | // TODO move to decls.w? 46 | // TODO create corresponding read? 47 | this.written = stringset(); 48 | 49 | // names of all variables declared outside this hoist scope but 50 | // referenced in this scope (immediately or in child). 51 | // only stored on hoist scopes for efficiency 52 | // (because we currently generate lots of empty block scopes) 53 | this.propagates = (this.kind === "hoist" ? stringset() : null); 54 | 55 | // scopes register themselves with their parents for easier traversal 56 | if (this.parent) { 57 | this.parent.children.push(this); 58 | } 59 | } 60 | 61 | Scope.prototype.print = function(indent) { 62 | indent = indent || 0; 63 | const scope = this; 64 | const names = this.decls.keys().map(function(name) { 65 | return fmt("{0} [{1}]", name, scope.decls.get(name).kind); 66 | }).join(", "); 67 | const propagates = this.propagates ? this.propagates.items().join(", ") : ""; 68 | console.log(fmt("{0}{1}: {2}. propagates: {3}", fmt.repeat(" ", indent), this.node.type, names, propagates)); 69 | this.children.forEach(function(c) { 70 | c.print(indent + 2); 71 | }); 72 | }; 73 | 74 | Scope.prototype.add = function(name, kind, node, referableFromPos) { 75 | assert(is.someof(kind, ["fun", "param", "var", "caught", "const", "let"])); 76 | 77 | function isConstLet(kind) { 78 | return is.someof(kind, ["const", "let"]); 79 | } 80 | 81 | let scope = this; 82 | 83 | // search nearest hoist-scope for fun, param and var's 84 | // const, let and caught variables go directly in the scope (which may be hoist, block or catch-block) 85 | if (is.someof(kind, ["fun", "param", "var"])) { 86 | while (scope.kind !== "hoist") { 87 | if (scope.decls.has(name) && isConstLet(scope.decls.get(name).kind)) { // could be caught 88 | return error(getline(node), "{0} is already declared", name); 89 | } 90 | scope = scope.parent; 91 | } 92 | } 93 | // name exists in scope and either new or existing kind is const|let => error 94 | if (scope.decls.has(name) && (options.disallowDuplicated || isConstLet(scope.decls.get(name).kind) || isConstLet(kind))) { 95 | return error(getline(node), "{0} is already declared", name); 96 | } 97 | 98 | const declaration = { 99 | kind: kind, 100 | node: node, 101 | }; 102 | if (referableFromPos) { 103 | assert(is.someof(kind, ["var", "const", "let"])); 104 | declaration.from = referableFromPos; 105 | } 106 | scope.decls.set(name, declaration); 107 | }; 108 | 109 | Scope.prototype.getKind = function(name) { 110 | assert(is.string(name)); 111 | const decl = this.decls.get(name); 112 | return decl ? decl.kind : null; 113 | }; 114 | 115 | Scope.prototype.getNode = function(name) { 116 | assert(is.string(name)); 117 | const decl = this.decls.get(name); 118 | return decl ? decl.node : null; 119 | }; 120 | 121 | Scope.prototype.getFromPos = function(name) { 122 | assert(is.string(name)); 123 | const decl = this.decls.get(name); 124 | return decl ? decl.from : null; 125 | }; 126 | 127 | Scope.prototype.hasOwn = function(name) { 128 | return this.decls.has(name); 129 | }; 130 | 131 | Scope.prototype.remove = function(name) { 132 | return this.decls.remove(name); 133 | }; 134 | 135 | Scope.prototype.doesPropagate = function(name) { 136 | return this.propagates.has(name); 137 | }; 138 | 139 | Scope.prototype.markPropagates = function(name) { 140 | this.propagates.add(name); 141 | }; 142 | 143 | Scope.prototype.closestHoistScope = function() { 144 | let scope = this; 145 | while (scope.kind !== "hoist") { 146 | scope = scope.parent; 147 | } 148 | return scope; 149 | }; 150 | 151 | Scope.prototype.hasFunctionScopeBetween = function(outer) { 152 | function isFunction(node) { 153 | return is.someof(node.type, ["FunctionDeclaration", "FunctionExpression"]); 154 | } 155 | 156 | for (let scope = this; scope; scope = scope.parent) { 157 | if (scope === outer) { 158 | return false; 159 | } 160 | if (isFunction(scope.node)) { 161 | return true; 162 | } 163 | } 164 | 165 | throw new Error("wasn't inner scope of outer"); 166 | }; 167 | 168 | Scope.prototype.lookup = function(name) { 169 | for (let scope = this; scope; scope = scope.parent) { 170 | if (scope.decls.has(name)) { 171 | return scope; 172 | } else if (scope.kind === "hoist") { 173 | scope.propagates.add(name); 174 | } 175 | } 176 | return null; 177 | }; 178 | 179 | Scope.prototype.markWrite = function(name) { 180 | assert(is.string(name)); 181 | this.written.add(name); 182 | }; 183 | 184 | // detects let variables that are never modified (ignores top-level) 185 | Scope.prototype.detectUnmodifiedLets = function() { 186 | const outmost = this; 187 | 188 | function detect(scope) { 189 | if (scope !== outmost) { 190 | scope.decls.keys().forEach(function(name) { 191 | if (scope.getKind(name) === "let" && !scope.written.has(name)) { 192 | return error(getline(scope.getNode(name)), "{0} is declared as let but never modified so could be const", name); 193 | } 194 | }); 195 | } 196 | 197 | scope.children.forEach(function(childScope) { 198 | detect(childScope); 199 | }); 200 | } 201 | detect(this); 202 | }; 203 | 204 | Scope.prototype.traverse = function(options) { 205 | options = options || {}; 206 | const pre = options.pre; 207 | const post = options.post; 208 | 209 | function visit(scope) { 210 | if (pre) { 211 | pre(scope); 212 | } 213 | scope.children.forEach(function(childScope) { 214 | visit(childScope); 215 | }); 216 | if (post) { 217 | post(scope); 218 | } 219 | } 220 | 221 | visit(this); 222 | }; 223 | 224 | module.exports = Scope; 225 | -------------------------------------------------------------------------------- /semantic-differences.md: -------------------------------------------------------------------------------- 1 | # Semantic differences between original ES6 and transpiled ES3 2 | 3 | ### Referenced (inside closure) before declaration 4 | `defs.js` detects the vast majority of cases where a variable is referenced prior to 5 | its declaration. The one case it cannot detect is the following: 6 | 7 | ```javascript 8 | function printx() { console.log(x); } 9 | printx(); // illegal 10 | let x = 1; 11 | printx(); // legal 12 | ``` 13 | 14 | The first call to `printx` is not legal because `x` hasn't been initialized at that point 15 | of *time*, which is impossible to catch reliably with statical analysis. 16 | `v8 --harmony` will detect and error on this via run-time checking. `defs.js` will 17 | happily transpile this example (`let` => `var` and that's it), and the transpiled code 18 | will print `undefined` on the first call to `printx`. This difference should be a very 19 | minor problem in practice. 20 | -------------------------------------------------------------------------------- /stats.js: -------------------------------------------------------------------------------- 1 | const fmt = require("simple-fmt"); 2 | const is = require("simple-is"); 3 | const assert = require("assert"); 4 | 5 | function Stats() { 6 | this.lets = 0; 7 | this.consts = 0; 8 | this.renames = []; 9 | } 10 | 11 | Stats.prototype.declarator = function(kind) { 12 | assert(is.someof(kind, ["const", "let"])); 13 | if (kind === "const") { 14 | this.consts++; 15 | } else { 16 | this.lets++; 17 | } 18 | }; 19 | 20 | Stats.prototype.rename = function(oldName, newName, line) { 21 | this.renames.push({ 22 | oldName: oldName, 23 | newName: newName, 24 | line: line, 25 | }); 26 | }; 27 | 28 | Stats.prototype.toString = function() { 29 | // console.log("defs.js stats for file {0}:", filename) 30 | 31 | const renames = this.renames.map(function(r) { 32 | return r; 33 | }).sort(function(a, b) { 34 | return a.line - b.line; 35 | }); // sort a copy of renames 36 | 37 | const renameStr = renames.map(function(rename) { 38 | return fmt("\nline {0}: {1} => {2}", rename.line, rename.oldName, rename.newName); 39 | }).join(""); 40 | 41 | const sum = this.consts + this.lets; 42 | const constlets = (sum === 0 ? 43 | "can't calculate const coverage (0 consts, 0 lets)" : 44 | fmt("{0}% const coverage ({1} consts, {2} lets)", 45 | Math.floor(100 * this.consts / sum), this.consts, this.lets)); 46 | 47 | return constlets + renameStr + "\n"; 48 | }; 49 | 50 | module.exports = Stats; 51 | -------------------------------------------------------------------------------- /tests/a-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function fn() { 3 | var x = 3; 4 | if (true) { 5 | var x$0 = 4; 6 | console.log(x$0); 7 | } 8 | console.log(x); 9 | } 10 | -------------------------------------------------------------------------------- /tests/a.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function fn() { 3 | var x = 3; 4 | if (true) { 5 | let x = 4; 6 | console.log(x); 7 | } 8 | console.log(x); 9 | } 10 | -------------------------------------------------------------------------------- /tests/allowed-loop-closures-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var arr = []; 3 | 4 | // can be transformed (common WAT) 5 | for (var x = 0; x < 10; x++) { 6 | arr.push(function() { return x; }); 7 | } 8 | 9 | // can be transformed (common manual work-around) 10 | for (var x$0 = 0; x$0 < 3; x$0++) { 11 | arr.push((function(x) { return function() { return x; } })(x$0)); 12 | } 13 | 14 | // can be transformed (no extra IIFE will be inserted) 15 | for (var x$1 = 0; x$1 < 3; x$1++) {(function(){ 16 | var y = 1; 17 | arr.push(function() { return y; }); 18 | }).call(this);} 19 | 20 | // can be transformed (added IIFE) 21 | for (var x$2 = 0; x$2 < 3; x$2++) {(function(){ 22 | var y = 1; 23 | arr.push(function() { return y; }); 24 | }).call(this);} 25 | 26 | // can be transformed (added IIFE) 27 | for (var x$3 = 0; x$3 < 3; x$3++) {(function(){ 28 | var y = x$3; 29 | arr.push(function() { return y; }); 30 | }).call(this);} 31 | 32 | // can be transformed (added IIFE) 33 | for (var x$4 = 0; x$4 < 3; x$4++) {(function(){ 34 | var y = x$4, z = arr.push(function() { return y; }); 35 | }).call(this);} 36 | 37 | // can be transformed (added IIFE) 38 | for (var x$5 = 0; x$5 < 3; x$5++) {(function(){ 39 | var x = 1; 40 | arr.push(function() { return x; }); 41 | }).call(this);} 42 | 43 | // can be transformed (added IIFE) 44 | while (true) { 45 | var f = function() { 46 | for (var x = 0; x < 10; x++) {(function(){ 47 | var y = x; 48 | arr.push(function() { return y; }); 49 | }).call(this);} 50 | }; 51 | f(); 52 | } 53 | 54 | // it's fine to use break, continue, return and arguments as long as 55 | // it's contained within a function below the loop so that it doesn't 56 | // interfere with the inserted IIFE 57 | (function() { 58 | for (var x = 0; x < 3; x++) {(function(){ 59 | var y = x; 60 | (function() { 61 | for(;;) break; 62 | return; 63 | })(); 64 | (function() { 65 | for(;;) continue; 66 | arguments 67 | })(); 68 | arr.push(function() { return y; }); 69 | }).call(this);} 70 | })(); 71 | 72 | // For-In 73 | for (var x$6 in [0,1,2]) {(function(x){ 74 | arr.push(function() { return x; }); 75 | }).call(this, x$6);} 76 | 77 | // Block-less For-In 78 | for (var x$7 in [0,1,2]) (function(x){arr.push(function() { return x; });}).call(this, x$7);/*with semicolon*/ 79 | for (var x$8 in [0,1,2]) (function(x){arr.push(function() { return x; })}).call(this, x$8);/*no semicolon*/ 80 | 81 | null; // previous semicolon-less for statement's range ends just before 'n' in 'null' 82 | 83 | // For-Of 84 | for (var x$9 of [0,1,2]) {(function(x){ 85 | arr.push(function() { return x; }); 86 | }).call(this, x$9);} 87 | 88 | // Block-less For-Of 89 | for (var x$10 of [0,1,2]) (function(x){arr.push(function() { return x; });}).call(this, x$10);/*with semicolon*/ 90 | for (var x$11 of [0,1,2]) (function(x){arr.push(function() { return x; })}).call(this, x$11);/*no semicolon*/ 91 | 92 | null; // previous semicolon-less for statement's range ends just before 'n' in 'null' 93 | 94 | // While 95 | while (true) {(function(){ 96 | var x = 1; 97 | arr.push(function() { return x; }); 98 | }).call(this);} 99 | 100 | // Do-While 101 | do {(function(){ 102 | var x = 1; 103 | arr.push(function() { return x; }); 104 | }).call(this);} while (true); 105 | 106 | arr.forEach(function(f) { 107 | console.log(f()); 108 | }); 109 | -------------------------------------------------------------------------------- /tests/allowed-loop-closures.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var arr = []; 3 | 4 | // can be transformed (common WAT) 5 | for (var x = 0; x < 10; x++) { 6 | arr.push(function() { return x; }); 7 | } 8 | 9 | // can be transformed (common manual work-around) 10 | for (let x = 0; x < 3; x++) { 11 | arr.push((function(x) { return function() { return x; } })(x)); 12 | } 13 | 14 | // can be transformed (no extra IIFE will be inserted) 15 | for (let x = 0; x < 3; x++) {(function(){ 16 | let y = 1; 17 | arr.push(function() { return y; }); 18 | }).call(this);} 19 | 20 | // can be transformed (added IIFE) 21 | for (let x = 0; x < 3; x++) { 22 | let y = 1; 23 | arr.push(function() { return y; }); 24 | } 25 | 26 | // can be transformed (added IIFE) 27 | for (let x = 0; x < 3; x++) { 28 | let y = x; 29 | arr.push(function() { return y; }); 30 | } 31 | 32 | // can be transformed (added IIFE) 33 | for (let x = 0; x < 3; x++) { 34 | let y = x, z = arr.push(function() { return y; }); 35 | } 36 | 37 | // can be transformed (added IIFE) 38 | for (let x = 0; x < 3; x++) { 39 | let x = 1; 40 | arr.push(function() { return x; }); 41 | } 42 | 43 | // can be transformed (added IIFE) 44 | while (true) { 45 | let f = function() { 46 | for (let x = 0; x < 10; x++) { 47 | let y = x; 48 | arr.push(function() { return y; }); 49 | } 50 | }; 51 | f(); 52 | } 53 | 54 | // it's fine to use break, continue, return and arguments as long as 55 | // it's contained within a function below the loop so that it doesn't 56 | // interfere with the inserted IIFE 57 | (function() { 58 | for (let x = 0; x < 3; x++) { 59 | let y = x; 60 | (function() { 61 | for(;;) break; 62 | return; 63 | })(); 64 | (function() { 65 | for(;;) continue; 66 | arguments 67 | })(); 68 | arr.push(function() { return y; }); 69 | } 70 | })(); 71 | 72 | // For-In 73 | for (let x in [0,1,2]) { 74 | arr.push(function() { return x; }); 75 | } 76 | 77 | // Block-less For-In 78 | for (let x in [0,1,2]) arr.push(function() { return x; });/*with semicolon*/ 79 | for (let x in [0,1,2]) arr.push(function() { return x; })/*no semicolon*/ 80 | 81 | null; // previous semicolon-less for statement's range ends just before 'n' in 'null' 82 | 83 | // For-Of 84 | for (let x of [0,1,2]) { 85 | arr.push(function() { return x; }); 86 | } 87 | 88 | // Block-less For-Of 89 | for (let x of [0,1,2]) arr.push(function() { return x; });/*with semicolon*/ 90 | for (let x of [0,1,2]) arr.push(function() { return x; })/*no semicolon*/ 91 | 92 | null; // previous semicolon-less for statement's range ends just before 'n' in 'null' 93 | 94 | // While 95 | while (true) { 96 | let x = 1; 97 | arr.push(function() { return x; }); 98 | } 99 | 100 | // Do-While 101 | do { 102 | let x = 1; 103 | arr.push(function() { return x; }); 104 | } while (true); 105 | 106 | arr.forEach(function(f) { 107 | console.log(f()); 108 | }); 109 | -------------------------------------------------------------------------------- /tests/catch-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var x = 1; 4 | { 5 | var x$0 = 3; 6 | try { 7 | } catch (x) { 8 | console.log(x); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/catch.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var x = 1; 4 | { 5 | let x = 3; 6 | try { 7 | } catch (x) { 8 | console.log(x); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/catch2-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //console.log(g); 4 | try { 5 | throw 1; 6 | } catch (e) { 7 | { 8 | var e$0 = 3; 9 | console.log(e$0); 10 | var g = 0; 11 | } 12 | console.log(e); 13 | } 14 | console.log(g); 15 | -------------------------------------------------------------------------------- /tests/catch2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //console.log(g); 4 | try { 5 | throw 1; 6 | } catch (e) { 7 | { 8 | let e = 3; 9 | console.log(e); 10 | var g = 0; 11 | } 12 | console.log(e); 13 | } 14 | console.log(g); 15 | -------------------------------------------------------------------------------- /tests/const-assign-stderr: -------------------------------------------------------------------------------- 1 | line 4: can't assign to const variable x 2 | line 5: can't assign to const variable x 3 | line 6: can't assign to const variable x 4 | line 7: can't assign to const variable x 5 | line 8: can't assign to const variable x 6 | -------------------------------------------------------------------------------- /tests/const-assign.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const x = 3; 4 | x++; 5 | --x; 6 | x = 4; 7 | x *= 2; 8 | x /= 2; 9 | 10 | if (true) { 11 | let x = 3; 12 | x++; 13 | } 14 | -------------------------------------------------------------------------------- /tests/duplicate-var-stderr: -------------------------------------------------------------------------------- 1 | line 5: x is already declared 2 | line 7: x is already declared 3 | -------------------------------------------------------------------------------- /tests/duplicate-var.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var x = 3; 4 | if (1) { 5 | var x = 4; 6 | } 7 | function x() { 8 | } 9 | -------------------------------------------------------------------------------- /tests/early-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x; 3 | function named_fn(a, b) { 4 | var unique = 1; 5 | for (x = 3; x < 10; x++) { 6 | var i = 1; 7 | var y$0 = 2; 8 | console.log(y$0); 9 | } 10 | for (var x$0 in [1,2,3,4,5]) { 11 | var y$1 = x$0; 12 | console.log(y$1); 13 | } 14 | var y,z; 15 | } 16 | named_fn(); 17 | -------------------------------------------------------------------------------- /tests/early.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x; 3 | function named_fn(a, b) { 4 | let unique = 1; 5 | for (x = 3; x < 10; x++) { 6 | var i = 1; 7 | let y = 2; 8 | console.log(y); 9 | } 10 | for (let x in [1,2,3,4,5]) { 11 | const y = x; 12 | console.log(y); 13 | } 14 | var y,z; 15 | } 16 | named_fn(); 17 | -------------------------------------------------------------------------------- /tests/forbidden-loop-closure-stderr: -------------------------------------------------------------------------------- 1 | line 11: Not yet specced ES6 feature. x is declared in for-loop header and then captured in loop closure 2 | line 14: Not yet specced ES6 feature. x is declared in for-loop header and then captured in loop closure 3 | line 22: Not yet specced ES6 feature. x is declared in for-loop header and then captured in loop closure 4 | line 32: loop-variable y is captured by a loop-closure that can't be transformed due to use of return at line 31 5 | line 40: loop-variable y is captured by a loop-closure that can't be transformed due to use of break at line 39 6 | line 47: loop-variable y is captured by a loop-closure that can't be transformed due to use of continue at line 46 7 | line 55: loop-variable y is captured by a loop-closure that can't be transformed due to use of arguments at line 54 8 | line 62: loop-variable y is captured by a loop-closure that can't be transformed due to use of var at line 61 9 | -------------------------------------------------------------------------------- /tests/forbidden-loop-closure.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var arr = []; 3 | 4 | // fresh x per iteration but semantics not determined yet 5 | // in ES6 spec draft (transfer in particular). Also inconsistent 6 | // between VM implementations. 7 | // once ES6 nails down the semantics (and VM's catch up) we'll 8 | // revisit 9 | // note v8 bug https://code.google.com/p/v8/issues/detail?id=2560 10 | // also see other/v8-bug.js 11 | for (let x = 0; x < 3; x++) { 12 | arr.push(function() { return x; }); 13 | } 14 | for (let z, x = 0; x < 3; x++) { 15 | arr.push(function() { return x; }); 16 | } 17 | 18 | // as a consequence of the above, defs is unable to transform 19 | // the code below (even though it is the output of an earlier 20 | // defs transformation). we should be able to detect this case 21 | // (and pass it through unmodified) but is it worth the effort? 22 | for (let x = 0; x < 3; x++) {(function(){ 23 | let y = x; 24 | arr.push(function() { return y; }); 25 | }).call(this);} 26 | 27 | // return is not allowed inside the loop body because the IIFE would break it 28 | (function() { 29 | for (let x = 0; x < 3; x++) { 30 | let y = x; 31 | return 1; 32 | arr.push(function() { return y; }); 33 | } 34 | })(); 35 | 36 | // break is not allowed inside the loop body because the IIFE would break it 37 | for (let x = 0; x < 3; x++) { 38 | let y = x; 39 | break; 40 | arr.push(function() { return y; }); 41 | } 42 | 43 | // continue is not allowed inside the loop body because the IIFE would break it 44 | for (let x = 0; x < 3; x++) { 45 | let y = x; 46 | continue; 47 | arr.push(function() { return y; }); 48 | } 49 | 50 | // arguments is not allowed inside the loop body because the IIFE would break it 51 | // (and I don't want to re-apply outer arguments in the inserted IIFE) 52 | for (let x = 0; x < 3; x++) { 53 | let y = x; 54 | arguments[0]; 55 | arr.push(function() { return y; }); 56 | } 57 | 58 | // continue is not allowed inside the loop body because the IIFE would break it 59 | for (let x = 0; x < 3; x++) { 60 | let y = x; 61 | var z = 1; 62 | arr.push(function() { return y; }); 63 | } 64 | 65 | // TODO block-less loops (is that even applicable?) 66 | 67 | arr.forEach(function(f) { 68 | console.log(f()); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/global-name-exists-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // unclear on whether it should be an error to redefine global names on top-level 3 | // for now, we allow it 4 | var name = "asdf"; 5 | var console = 3; 6 | var undefined = 5; 7 | -------------------------------------------------------------------------------- /tests/global-name-exists.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // unclear on whether it should be an error to redefine global names on top-level 3 | // for now, we allow it 4 | let name = "asdf"; 5 | let console = 3; 6 | let undefined = 5; 7 | -------------------------------------------------------------------------------- /tests/let-already-declared-stderr: -------------------------------------------------------------------------------- 1 | line 4: x is already declared 2 | line 5: x is already declared 3 | line 9: f is already declared 4 | line 14: y is already declared 5 | -------------------------------------------------------------------------------- /tests/let-already-declared.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var x = 3; 4 | let x = 4; 5 | const x = 5; 6 | 7 | function f() { 8 | let f = 1; 9 | function f() { 10 | } 11 | 12 | const y = 1; 13 | if (1) { 14 | var y; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/letletlet-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x; 3 | { var x$0; } 4 | { var x$1; } 5 | { var x$2; } 6 | 7 | { var y; } 8 | { var y$0; } 9 | { var y$1; } 10 | 11 | { var z; { var z$0; }} 12 | { var z$1; } 13 | { var z$2; { var z$3; { var z$4; }}} 14 | -------------------------------------------------------------------------------- /tests/letletlet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x; 3 | { let x; } 4 | { let x; } 5 | { let x; } 6 | 7 | { let y; } 8 | { let y; } 9 | { let y; } 10 | 11 | { let z; { let z; }} 12 | { let z; } 13 | { let z; { let z; { let z; }}} 14 | -------------------------------------------------------------------------------- /tests/named-function-expression-conservative-error-stderr: -------------------------------------------------------------------------------- 1 | line 4: a is already declared 2 | line 11: f is already declared 3 | -------------------------------------------------------------------------------- /tests/named-function-expression-conservative-error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fn1 = function f(a) { 3 | // not allowed and defs produces an error accordingly 4 | const a = 3; 5 | } 6 | const fn2 = function f(a) { 7 | // defs produces an error but it's a false positive 8 | // (it's allowed per the ES spec). Being conservative 9 | // is a good thing here because the usage is unnecessary 10 | // and error-prone 11 | const f = 3; 12 | } 13 | -------------------------------------------------------------------------------- /tests/named-function-expression-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var f1 = function named_f() { 4 | console.log(named_f); 5 | }; 6 | 7 | var f2 = function named_f() { 8 | console.log(named_f); 9 | if (true) { 10 | var named_f$0 = 1; 11 | console.log(named_f$0); 12 | } 13 | }; 14 | 15 | var named_g = function() {}; 16 | if (true) { 17 | var named_g$0 = function() {}; // renamed 18 | var f3 = function named_g() { // stays 19 | console.log(named_g); // stays 20 | }; 21 | console.log(named_g$0); // renamed 22 | } 23 | console.log(named_g) 24 | -------------------------------------------------------------------------------- /tests/named-function-expression.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const f1 = function named_f() { 4 | console.log(named_f); 5 | }; 6 | 7 | const f2 = function named_f() { 8 | console.log(named_f); 9 | if (true) { 10 | const named_f = 1; 11 | console.log(named_f); 12 | } 13 | }; 14 | 15 | const named_g = function() {}; 16 | if (true) { 17 | const named_g = function() {}; // renamed 18 | const f3 = function named_g() { // stays 19 | console.log(named_g); // stays 20 | }; 21 | console.log(named_g); // renamed 22 | } 23 | console.log(named_g) 24 | -------------------------------------------------------------------------------- /tests/rename-array-index-out.js: -------------------------------------------------------------------------------- 1 | var arr = [1, 2, 3]; 2 | var i = 0; 3 | 4 | for (var i$0 = 0; i$0 < arr.length; i$0++) { 5 | arr[i$0]; 6 | i$0[i$0]; 7 | } 8 | -------------------------------------------------------------------------------- /tests/rename-array-index.js: -------------------------------------------------------------------------------- 1 | var arr = [1, 2, 3]; 2 | var i = 0; 3 | 4 | for (let i = 0; i < arr.length; i++) { 5 | arr[i]; 6 | i[i]; 7 | } 8 | -------------------------------------------------------------------------------- /tests/rename-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x = "x"; 3 | function named_fn(a, b) { 4 | if (true) { 5 | (function() { 6 | console.log(x); 7 | })(); 8 | } 9 | 10 | // let x must be renamed or else it will shadow the reference on line 5 11 | for (var x$0 = 0; x$0 < 2; x$0++) { 12 | console.log(x$0); 13 | } 14 | } 15 | named_fn(); 16 | -------------------------------------------------------------------------------- /tests/rename.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let x = "x"; 3 | function named_fn(a, b) { 4 | if (true) { 5 | (function() { 6 | console.log(x); 7 | })(); 8 | } 9 | 10 | // let x must be renamed or else it will shadow the reference on line 5 11 | for (let x = 0; x < 2; x++) { 12 | console.log(x); 13 | } 14 | } 15 | named_fn(); 16 | -------------------------------------------------------------------------------- /tests/use-before-definition-stderr: -------------------------------------------------------------------------------- 1 | line 3: reference to unknown global variable x 2 | line 5: x is referenced before its declaration 3 | line 7: x is referenced before its declaration 4 | -------------------------------------------------------------------------------- /tests/use-before-definition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | x; // error (unless disallowUnknownReferences=false) 4 | if (true) { 5 | x; // error 6 | if (true) { 7 | x; // error 8 | } 9 | if (true) { 10 | let x; 11 | x; // ok 12 | } 13 | let f = function() { 14 | return x; // ok 15 | }; 16 | f(); // ok from a static analysis standpoint but runtime error in ES6 17 | 18 | let x = 3; 19 | 20 | f(); // ok 21 | x; // ok 22 | if (true) { 23 | x; // ok 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/used-in-same-declaration-stderr: -------------------------------------------------------------------------------- 1 | line 2: x is referenced before its declaration 2 | line 4: b is referenced before its declaration 3 | -------------------------------------------------------------------------------- /tests/used-in-same-declaration.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let x = x; // error 3 | let y = 1, z = y; // ok 4 | let a = b, b = 1; // error 5 | -------------------------------------------------------------------------------- /tests/var-inside-let-stderr: -------------------------------------------------------------------------------- 1 | line 9: x is already declared 2 | line 15: y is already declared 3 | line 22: z is already declared 4 | -------------------------------------------------------------------------------- /tests/var-inside-let.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // all three var x|y|z = 4 should be errors I believe so we error on all 4 | // v8 --harmony allows the first but not the second and last 5 | 6 | if (true) { 7 | let x = 3; 8 | if (true) { 9 | var x = 4; 10 | } 11 | } 12 | 13 | let y = 3; 14 | if (true) { 15 | var y = 4; 16 | } 17 | 18 | (function() { 19 | if (true) { 20 | let z = 3; 21 | if (true) { 22 | var z = 4; 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /tests/var-let-same-scope-stderr: -------------------------------------------------------------------------------- 1 | line 3: x is already declared 2 | line 6: y is already declared 3 | -------------------------------------------------------------------------------- /tests/var-let-same-scope.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x = 4; 3 | let x = 5; 4 | 5 | let y = 6; 6 | var y = 7; 7 | -------------------------------------------------------------------------------- /tests/xdollarzero-out.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x$0; 3 | var x; 4 | { var x$1; } 5 | -------------------------------------------------------------------------------- /tests/xdollarzero.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var x$0; 3 | var x; 4 | { let x; } 5 | --------------------------------------------------------------------------------