├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bower.json ├── build ├── aether.js ├── aether.min.js ├── coffeescript.js ├── coffeescript.min.js ├── dev │ ├── index.css │ ├── index.css.map │ └── index.js ├── html.js ├── html.min.js ├── index.html ├── java.js ├── java.min.js ├── javascript.js ├── javascript.min.js ├── lua.js ├── lua.min.js ├── python.js └── python.min.js ├── dev ├── demo.jade ├── develop.jade ├── index.coffee ├── index.jade ├── index.sass ├── overview.jade └── usage.jade ├── package.json ├── parsers ├── coffeescript.js ├── html.js ├── java.js ├── javascript.js ├── lua.js └── python.js ├── rt.coffee ├── sc.coffee ├── src ├── aether.coffee ├── defaults.coffee ├── execution.coffee ├── interpreter.coffee ├── languages │ ├── coffeescript.coffee │ ├── html.coffee │ ├── java.coffee │ ├── javascript.coffee │ ├── language.coffee │ ├── languages.coffee │ ├── lua.coffee │ └── python.coffee ├── problems.coffee ├── protectBuiltins.coffee ├── ranges.coffee ├── transforms.coffee ├── traversal.coffee └── validators │ └── options.coffee └── test ├── aether_spec.coffee ├── constructor_spec.coffee ├── cs_spec.coffee ├── es6_spec.coffee ├── flow_spec.coffee ├── global_scope_spec.coffee ├── java_errors_spec.coffee ├── java_milestones_spec.ec5 ├── java_spec.coffee ├── lint_spec.coffee ├── lua_spec.coffee ├── problem_spec.coffee ├── python_spec.coffee └── statement_count_spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't include intermediate JS 2 | lib 3 | 4 | lib-cov 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.gz 12 | 13 | node_modules/ 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | 20 | # OS X 21 | .DS_Store 22 | Icon? 23 | ._* 24 | .Spotlight-V100 25 | .Trashes 26 | 27 | # Windows 28 | Thumbs.db 29 | 30 | # Emacs 31 | *.*~ 32 | *.# 33 | .#* 34 | *# 35 | 36 | # Vim 37 | .*.sw[a-z] 38 | *.un~i 39 | 40 | # Sublime 41 | *.sublime-project 42 | *.sublime-workspace 43 | 44 | *.diff 45 | *.err 46 | *.orig 47 | *.log 48 | *.rej 49 | *.vi 50 | *.sass-cache 51 | 52 | # OS or Editor folders 53 | .cache 54 | .project 55 | .settings 56 | .tmproj 57 | .idea 58 | nbproject 59 | 60 | .grunt 61 | coverage/ 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Don't include GitHub stuff or local scripts 2 | Gruntfile.coffee 3 | .gitignore 4 | .travis.yml 5 | 6 | # Only provide compiled JS output, not browserified 7 | src 8 | build 9 | test 10 | lib/test 11 | 12 | ### Stuff from .gitignore ### 13 | 14 | # OS X 15 | .DS_Store 16 | Icon? 17 | ._* 18 | .Spotlight-V100 19 | .Trashes 20 | 21 | # Windows 22 | Thumbs.db 23 | 24 | # Emacs 25 | *.*~ 26 | *.# 27 | .#* 28 | *# 29 | 30 | # Vim 31 | .*.sw[a-z] 32 | *.un~i 33 | 34 | # Sublime 35 | *.sublime-project 36 | *.sublime-workspace 37 | 38 | *.diff 39 | *.err 40 | *.orig 41 | *.log 42 | *.rej 43 | *.vi 44 | *.sass-cache 45 | 46 | # OS or Editor folders 47 | .cache 48 | .project 49 | .settings 50 | .tmproj 51 | .idea 52 | nbproject 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | before_script: 6 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Please sign our Contributor License Agreement 2 | 3 | **[http://codecombat.com/cla](http://codecombat.com/cla)** 4 | 5 | It just grants us a non-exclusive license to use your contribution and certifies you have the right to contribute the code you submit. For both our sakes, we need this before we can accept a pull request. Don't worry, it's super easy. 6 | 7 | For more info, see [http://codecombat.com/legal](http://codecombat.com/legal). -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # Project configuration. 4 | grunt.initConfig 5 | pkg: grunt.file.readJSON 'package.json' 6 | uglify: 7 | options: 8 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' 9 | 10 | build: 11 | src: 'build/<%= pkg.name %>.js' 12 | dest: 'build/<%= pkg.name %>.min.js' 13 | 14 | parsers: 15 | files: [ 16 | {src: 'build/python.js', dest: 'build/python.min.js'} 17 | {src: 'build/lua.js', dest: 'build/lua.min.js'} 18 | {src: 'build/coffeescript.js', dest: 'build/coffeescript.min.js'} 19 | {src: 'build/javascript.js', dest: 'build/javascript.min.js'} 20 | {src: 'build/java.js', dest: 'build/java.min.js'} 21 | {src: 'build/html.js', dest: 'build/html.min.js'} 22 | ] 23 | 24 | coffeelint: 25 | app: ['src/*.coffee', 'test/*.coffee', 'dev/*.coffee'] 26 | options: 27 | no_trailing_whitespace: 28 | # PyCharm can't just autostrip for .coffee, needed for .jade 29 | level: 'ignore' 30 | max_line_length: 31 | level: 'ignore' 32 | line_endings: 33 | value: "unix" 34 | level: "error" 35 | 36 | watch: 37 | files: ['src/**/*', 'test/**/*.coffee', 'dev/**/*.coffee'] 38 | tasks: ['coffeelint', 'coffee', 'browserify', 'jasmine_node'] 39 | options: 40 | spawn: true 41 | interrupt: true 42 | atBegin: true 43 | livereload: true 44 | 45 | #jasmine: 46 | # aetherTests: 47 | # src: ['build/<%= pkg.name %>.js'] 48 | # options: 49 | # specs: [''] 50 | 51 | jasmine_node: 52 | run: 53 | spec: "lib/test/" 54 | runCoverage: 55 | spec: "coverage/instrument/lib/test" 56 | env: 57 | NODE_PATH: "lib" 58 | executable: './node_modules/.bin/jasmine_node' 59 | 60 | coffee: 61 | compile: 62 | files: [ 63 | expand: true # Enable dynamic expansion. 64 | cwd: 'src/' # Src matches are relative to this path. 65 | src: ['**/*.coffee'] # Actual pattern(s) to match. 66 | dest: 'lib/' # Destination path prefix. 67 | ext: '.js' # Dest filepaths will have this extension. 68 | , 69 | expand: true # Enable dynamic expansion. 70 | cwd: 'test/' # Src matches are relative to this path. 71 | src: ['**/*.coffee'] # Actual pattern(s) to match. 72 | dest: 'lib/test/' # Destination path prefix. 73 | ext: '.js' # Dest filepaths will have this extension. 74 | ] 75 | dev: 76 | files: 77 | 'build/dev/index.js': 'dev/index.coffee' 78 | 79 | browserify: 80 | src: 81 | src: ['lib/<%= pkg.name %>.js'] 82 | dest: 'build/<%= pkg.name %>.js' 83 | options: 84 | #standalone: "Aether" # can't figure out how to get this to work 85 | ignore: ['lodash', 'filbert', 86 | 'filbert/filbert_loose', 'lua2js', 87 | 'coffee-script-redux', 'jshint', 'cashew-js', 88 | 'esper.js', 'deku', 'htmlparser2'] 89 | parsers: 90 | files: [ 91 | {src: 'parsers/python.js', dest: 'build/python.js'} 92 | {src: 'parsers/lua.js', dest: 'build/lua.js'} 93 | {src: 'parsers/coffeescript.js', dest: 'build/coffeescript.js'} 94 | {src: 'parsers/javascript.js', dest: 'build/javascript.js'} 95 | {src: 'parsers/java.js', dest: 'build/java.js'} 96 | {src: 'parsers/html.js', dest: 'build/html.js'} 97 | ] 98 | # We're not using jasmine but now jasmine_node, 99 | # so we don't need to browserify the tests 100 | #test: 101 | # src: ['lib/test/*.js'] 102 | # dest: 'build/test/<%= pkg.name %>_specs.js' 103 | 104 | 'string-replace': 105 | build: 106 | files: 107 | 'build/<%= pkg.name %>.js': 'build/<%= pkg.name %>.js' 108 | options: 109 | replacements: [ 110 | {pattern: /\$defineProperty\(Object, 'assign', method\(assign\)\);/, replacement: "//$defineProperty(Object, 'assign', method(assign)); // This polyfill interferes with Facebook's JS SDK and isn't needed for our use case anyway."} 111 | ] 112 | 113 | "gh-pages": 114 | options: 115 | base: 'build' 116 | src: ['**/*'] 117 | 118 | push: 119 | options: 120 | files: ['package.json', 'bower.json'] 121 | updateConfigs: ['pkg'] 122 | commitMessage: 'Release %VERSION%' 123 | commitFiles: ['-a'] 124 | tagName: '%VERSION%' 125 | npm: true 126 | 127 | jade: 128 | dev: 129 | options: 130 | pretty: true 131 | data: '<%= pkg %>' 132 | files: 133 | 'build/index.html': ['dev/index.jade'] 134 | 135 | sass: 136 | dev: 137 | options: 138 | trace: true 139 | # no need to depend on sass gem 3.3.0 before it's out 140 | #sourcemap: true 141 | unixNewlines: true 142 | noCache: true 143 | files: 144 | 'build/dev/index.css': ['dev/index.sass'] 145 | 146 | instrument: 147 | files: "lib/**/*.js" 148 | options: 149 | lazy: true 150 | basePath: "coverage/instrument" 151 | 152 | copy: 153 | jstests: 154 | src: "test/java_milestones_spec.ec5" 155 | dest: "lib/test/java_milestones_spec.js" 156 | tests: 157 | expand: true 158 | flatten: true 159 | src: "lib/test/**/*" 160 | dest: "coverage/instrument/lib/test/" 161 | 162 | storeCoverage: 163 | options: 164 | dir: "coverage/reports" 165 | 166 | makeReport: 167 | src: "coverage/reports/**/*.json" 168 | options: 169 | type: "lcov" 170 | dir: "coverage/reports" 171 | print: "detail" 172 | 173 | # Load the plugin that provides the "uglify" task. 174 | grunt.loadNpmTasks 'grunt-contrib-uglify' 175 | #grunt.loadNpmTasks 'grunt-contrib-jasmine' 176 | grunt.loadNpmTasks 'grunt-newer' 177 | grunt.loadNpmTasks 'grunt-jasmine-node' 178 | grunt.loadNpmTasks 'grunt-coffeelint' 179 | grunt.loadNpmTasks 'grunt-contrib-coffee' 180 | grunt.loadNpmTasks 'grunt-contrib-watch' 181 | grunt.loadNpmTasks 'grunt-browserify' 182 | grunt.loadNpmTasks 'grunt-string-replace' 183 | grunt.loadNpmTasks 'grunt-notify' 184 | grunt.loadNpmTasks 'grunt-gh-pages' 185 | grunt.loadNpmTasks 'grunt-push-release' 186 | grunt.loadNpmTasks 'grunt-contrib-jade' 187 | grunt.loadNpmTasks 'grunt-contrib-sass' 188 | grunt.loadNpmTasks 'grunt-istanbul' 189 | grunt.loadNpmTasks 'grunt-contrib-copy' 190 | 191 | # Default task(s). 192 | grunt.registerTask 'default', ['coffeelint', 'coffee', 'browserify', 193 | 'string-replace', 'copy:jstests', 'jasmine_node:run', 'jade', 'sass'] #, 'uglify'] 194 | grunt.registerTask 'travis', ['coffeelint', 'coffee', 'copy:jstests', 'jasmine_node:run'] 195 | grunt.registerTask 'test', ['newer:coffee', 'copy:jstests', 'jasmine_node:run'] 196 | grunt.registerTask 'coverage', ['coffee', 'instrument', 'copy:tests', 197 | 'jasmine_node:runCoverage', 'storeCoverage', 'makeReport'] 198 | grunt.registerTask 'build', ['coffeelint', 'coffee', 'browserify:src', 199 | 'string-replace', 'jade', 'sass', 'uglify:build'] 200 | grunt.registerTask 'parsers', ['browserify:parsers', 'uglify:parsers'] 201 | 202 | # Run a single test with `grunt spec:filename`. 203 | # For example, `grunt spec:io` will run tests in io_spec.coffee 204 | grunt.registerTask 'spec', 'Run jasmine_node test on a specified file', (filename) -> 205 | grunt.task.run 'newer:coffee' 206 | grunt.config.set 'jasmine_node.match', filename 207 | grunt.config.set 'jasmine_node.matchAll', false 208 | grunt.config.set 'jasmine_node.specNameMatcher', '_spec' 209 | grunt.task.run 'jasmine_node:run' 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 CodeCombat Inc. and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/codecombat/aether.png)](https://travis-ci.org/codecombat/aether) 2 | 3 | ## Purpose 4 | Aether aims to make it easy for people to learn and write JavaScript and CoffeeScript by helping them catch and fix bugs, 5 | letting them see and more easily understand the execution of their program [(like Bret Victor commanded!)](http://youtu.be/PUv66718DII?t=17m25s), 6 | and giving them tips on how they can improve their code. [CodeCombat](http://codecombat.com/) is behind it. 7 | 8 | ## Get in touch 9 | You can use the [GitHub issues](https://github.com/codecombat/aether/issues), the [Discourse forum](http://discourse.codecombat.com/), the [HipChat](http://www.hipchat.com/g3plnOKqa), or [email](mailto:nick@codecombat.com) [Nick](http://www.nickwinter.net/). 10 | 11 | ## What is it? 12 | It's a JavaScript library (written in CoffeeScript) that takes user code as input; does computer-sciencey transpilation things to it with the help of [JSHint](http://jshint.com/), [Esprima](http://esprima.org/), [escodegen](https://github.com/Constellation/escodegen), and Esper (soon to be open source); and gives you linting, transformation, sandboxing, instrumentation, time-travel debugging, style analysis, autocompletion, and more. It used to output transpiled code for you to run, but now it includes an interpreter for better correctness and performance. 13 | 14 | ## Devour [aetherjs.com](http://aetherjs.com) for docs, demos, and developer discourse 15 | 16 | ## License 17 | [The MIT License (MIT)](https://github.com/codecombat/aether/blob/master/LICENSE) 18 | 19 | If you'd like to contribute, please [sign the CodeCombat contributor license agreement](http://codecombat.com/cla) so we can accept your pull requests. It is easy. 20 | 21 | ====== 22 | 23 | ![aether logo](http://i.imgur.com/uf36eRD.jpg) 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aether", 3 | "version": "0.5.40", 4 | "homepage": "https://github.com/codecombat/aether", 5 | "authors": [ 6 | "CodeCombat " 7 | ], 8 | "description": "Analyzes, instruments, and transpiles JS to help beginners.", 9 | "main": "build/aether.js", 10 | "keywords": [ 11 | "lint", 12 | "static", 13 | "analysis", 14 | "transpiler", 15 | "learning", 16 | "programming" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "test", 23 | "src", 24 | "lib", 25 | "build/test", 26 | "build/dev", 27 | "build/index.html", 28 | "Gruntfile.coffee", 29 | "plan.md" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /build/dev/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #21c0fb url("http://i.imgur.com/uf36eRD.jpg") no-repeat center center fixed; 3 | background-size: contain; } 4 | 5 | .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { 6 | background-color: #21c0fb; } 7 | 8 | #content { 9 | box-shadow: 0 0 6px; 10 | margin: 0 auto; 11 | width: 800px; 12 | background: rgba(255, 255, 255, 0.95); 13 | padding: 20px; } 14 | 15 | h1 { 16 | border-radius: 3px; 17 | padding: 15px 10px; 18 | background: #21c0fb; 19 | color: white; 20 | text-shadow: 1px 1px 1px black; 21 | margin-bottom: 0px; } 22 | 23 | .nav { 24 | padding: 0 5px; 25 | margin-top: 5px; } 26 | 27 | #github-link { 28 | float: right; 29 | background-color: rgba(0, 0, 0, 0.1); 30 | border: 1px solid rgba(0, 0, 0, 0.2); 31 | border-radius: 2px; } 32 | 33 | #demo h3 { 34 | margin-top: 40px; } 35 | 36 | h4 { 37 | margin: 20px 0; } 38 | 39 | li { 40 | margin: 4px 0; } 41 | 42 | .tab-pane { 43 | padding: 10px; 44 | margin-bottom: 20px; 45 | border-left: 1px solid lightgray; 46 | border-right: 1px solid lightgray; 47 | box-shadow: 0 0 10px #aaa; } 48 | 49 | .nav-tabs li { 50 | z-index: 1; } 51 | 52 | pre { 53 | margin-bottom: 20px; } 54 | pre.tab-pane { 55 | border: 0; } 56 | 57 | footer { 58 | margin-top: 50px; 59 | text-align: center; 60 | clear: both; } 61 | footer div { 62 | background-color: #666; } 63 | footer .bar { 64 | margin-top: 10px; 65 | width: 100%; } 66 | footer div:first-child { 67 | height: 5px; } 68 | footer div:last-child { 69 | height: 20px; 70 | margin-top: 10px; } 71 | 72 | pre { 73 | font-size: 12px; } 74 | 75 | #demo { 76 | background: white; 77 | position: absolute; 78 | left: 20px; 79 | right: 20px; 80 | top: 180px; } 81 | #demo .ace-editor-wrapper { 82 | position: absolute; 83 | top: 35px; 84 | right: 20px; 85 | bottom: 0; 86 | left: 20px; 87 | height: 380px; } 88 | 89 | #aether-problems, #aether-console, #aether-metrics, #aether-flow { 90 | overflow: scroll; 91 | max-height: 380px; } 92 | 93 | #demo #aether-metrics { 94 | height: 200px; } 95 | #demo #aether-console { 96 | height: 100px; } 97 | 98 | .ace_editor .executed { 99 | background-color: rgba(216, 88, 44, 0.125); 100 | position: absolute; } 101 | 102 | .demo-row { 103 | height: 430px; } 104 | 105 | .dev-section { 106 | background-color: #fafafa; } 107 | 108 | #worst-problem-wrapper { 109 | position: absolute; 110 | z-index: 100; 111 | top: 445px; 112 | left: 60px; 113 | font-size: 20px; 114 | background-color: rgba(240, 240, 255, 0.8); } 115 | #worst-problem-wrapper.error { 116 | color: red; } 117 | #worst-problem-wrapper.warning { 118 | color: orange; } 119 | #worst-problem-wrapper.info { 120 | color: green; } 121 | 122 | /*# sourceMappingURL=index.css.map */ 123 | -------------------------------------------------------------------------------- /build/dev/index.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAEA,IAAI;EACF,UAAU,EAAE,2EAA4E;EACxF,eAAe,EAAE,OAAO;;AAE1B,8FAAkF;EAChF,gBAAgB,EAPR,OAAqB;;AAS/B,QAAQ;EACN,UAAU,EAAE,OAAO;EACnB,MAAM,EAAE,MAAM;EACd,KAAK,EAAE,KAAK;EACZ,UAAU,EAAE,yBAAyB;EACrC,OAAO,EAAE,IAAI;;AAEf,EAAE;EACA,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,UAAU,EAnBF,OAAqB;EAoB7B,KAAK,EAAE,KAAK;EACZ,WAAW,EAAE,iBAAiB;EAC9B,aAAa,EAAE,GAAG;;AAEpB,IAAI;EACF,OAAO,EAAE,KAAK;EACd,UAAU,EAAE,GAAG;;AAEjB,YAAY;EACV,KAAK,EAAE,KAAK;EACZ,gBAAgB,EAAE,kBAAkB;EACpC,MAAM,EAAE,4BAA4B;EACpC,aAAa,EAAE,GAAG;;AAEpB,QAAQ;EACN,UAAU,EAAE,IAAI;;AAElB,EAAE;EACA,MAAM,EAAE,MAAM;;AAEhB,EAAE;EACA,MAAM,EAAE,KAAK;;AAIf,SAAS;EACP,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,mBAAmB;EAChC,YAAY,EAAE,mBAAmB;EACjC,UAAU,EAAE,aAAa;;AAE3B,YAAY;EACV,OAAO,EAAE,CAAC;;AAEZ,GAAG;EAGD,aAAa,EAAE,IAAI;EAFnB,YAAU;IACR,MAAM,EAAE,CAAC;;AAKb,MAAM;EACJ,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,MAAM;EAClB,KAAK,EAAE,IAAI;EACX,UAAG;IACD,gBAAgB,EAAE,IAAI;EACxB,WAAI;IACF,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,IAAI;EAEX,sBAAa;IACX,MAAM,EAAE,GAAG;EACb,qBAAY;IACV,MAAM,EAAE,IAAI;IACZ,UAAU,EAAE,IAAI;;AAEtB,GAAG;EACD,SAAS,EAAE,IAAI;;AAEjB,KAAK;EACH,UAAU,EAAE,KAAK;EACjB,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,IAAI;EACV,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,KAAK;EACV,yBAAmB;IACjB,QAAQ,EAAE,QAAQ;IAClB,GAAG,EAAE,IAAI;IACT,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,IAAI;IACV,MAAM,EAAE,KAAK;;AAEjB,gEAAgE;EAC9D,QAAQ,EAAE,MAAM;EAChB,UAAU,EAAE,KAAK;;AAGjB,qBAAe;EACb,MAAM,EAAE,KAAK;AACf,qBAAe;EACb,MAAM,EAAE,KAAK;;AAEjB,qBAAqB;EACnB,gBAAgB,EAAE,wBAAwB;EAC1C,QAAQ,EAAE,QAAQ;;AAEpB,SAAS;EACP,MAAM,EAAE,KAAK;;AAEf,YAAY;EACV,gBAAgB,EAAE,OAAO;;AAE3B,sBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,GAAG;EACZ,GAAG,EAAE,KAAK;EACV,IAAI,EAAE,IAAI;EACV,SAAS,EAAE,IAAI;EACf,gBAAgB,EAAE,wBAAwB;EAC1C,4BAAO;IACL,KAAK,EAAE,GAAG;EACZ,8BAAS;IACP,KAAK,EAAE,MAAM;EACf,2BAAM;IACJ,KAAK,EAAE,KAAK", 4 | "sources": ["../../dev/index.sass"], 5 | "names": [], 6 | "file": "index.css" 7 | } 8 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Aether 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Fork me on GitHub 28 |

Aether - Purify Your Users' JavaScript

29 | 36 |
37 |
38 |
39 |

Aether aims to make it easy for people to learn and write JavaScript and CoffeeScript by helping them catch and fix bugs, 40 | letting them see and more easily understand the execution of their program (like Bret Victor commanded!), and giving them tips on how they can improve their code. CodeCombat is behind it. 41 |

42 |
43 |

Get in touch

44 |

You can use the GitHub issues, the Discourse forum, the Google Group, the HipChat, or email Nick.

45 |

What is it?

46 |

It's a JavaScript library (written in CoffeeScript) that takes user code as input; does computer-sciencey transpilation things to it with the help of JSHint, Esprima, JS_WALA, escodegen, and traceur; and gives you:

47 |
    48 |
  1. 49 |

    incomplete Linting with totally configurable error/warning/info messages. Instead of "Warning: Line 2: Missing semicolon.", you could make Aether say, "Error: Put a semicolon here on line 2 after 'this.explode()' to end the statement.", or "fyi: might want a ; here amiga", or nothing at all. Aether does spellchecking based on what properties should exist, too: Should be 'destroy', not 'destory'.

    50 |
  2. 51 |
  3. 52 |

    Transformation like with node-falafel, but even easier, because your source transformation plugins can run either before or after the AST is normalized with JS_WALA.

    53 |
  4. 54 |
  5. 55 |

    incomplete Sandboxing so that you can actually run user code in the browser without worrying if the world will end. Well, not actually. That is hard; one should at least use a web worker. But good enough to foil most hacking attempts.

    56 |
  6. 57 |
  7. 58 |

    Instrumentation so that when the code is run, you'll know everything that happened. Count statements executed, step through the flow of execution, retrieve past variable states, and even pause and resume execution whenever you want. Pause after every statement? Sure!

    59 |
  8. 60 |
  9. 61 |

    incomplete Time-travel debugging to combine and surpass the best parts of both stepping debugging and logging.

    62 |
  10. 63 |
  11. 64 |

    planned Style analysis: metrics on things like what kind of indentation and bracket style the code uses, whether it uses recursion or regular expressions, etc.

    65 |
  12. 66 |
  13. 67 |

    planned Autocompletion suggestions based on unfinished code.

    68 |
  14. 69 |
  15. 70 |

    planned Other goodies! You can let your users code in ES6 now and hopefully CoffeeScript soon.

    71 |
  16. 72 |
73 |

Development Status

74 |

Alpha–watch out! CodeCombat is using it right now, but there are many bugs and missing pieces. If you'd be interested in Aether were it solid and finished, please tell us so we can get an idea of where else it might be useful and how it should work.

75 |

How does it work?

76 |

Aether uses JSHint, Esprima, Acorn, JS_WALA, escodegen, and traceur together to carry out this process:

77 |
    78 |
  1. We use JSHint to provide lint warnings and errors.
  2. 79 |
  3. We wrap the user code in a function declaration, since Aether expects the user code to be the body of a function.
  4. 80 |
  5. We do a regexp replace step to check for common mistakes. This will probably go away.
  6. 81 |
  7. We parse it with Esprima to get an AST. If it's invalid, we fall back to parsing with Acorn in loose mode so that we can get a workable AST despite the errors.
  8. 82 |
  9. We run a bunch of pre-normalization transformations on the AST to grab variable declarations, original statement ranges, and check for several more types of mistakes.
  10. 83 |
  11. We use JS_WALA to normalize the AST so that there'll be far fewer cases to handle in the next transformation step.
  12. 84 |
  13. We output that transformed AST to JS again using escodegen, since our transformations need to operate on an AST with matching original source at the same time.
  14. 85 |
  15. We parse again with Esprima and run a bunch of post-normalization transformations on that AST to do things like inserting instrumentation, protecting external objects, and adding yield statements.
  16. 86 |
  17. We use traceur to convert our ES6 yield statements to giant ES5 state machines to simulate generators, if we added any yields.
  18. 87 |
  19. We add one more function to intercept references to this for security.
  20. 88 |
89 |

License

90 |

The MIT License (MIT)

91 |

If you'd like to contribute, please sign the CodeCombat contributor license agreement so we can accept your pull requests. It is easy.

92 |
93 |
94 |

Load example: 95 | 96 |

97 |
98 |
99 |
100 |

Write some JavaScript here.

101 |
102 |
103 |
104 |

Use Aether on it.

105 |
106 |
107 |
108 |

Problems detected show up here.

109 |
110 |
111 |
112 |
113 |
114 |

Gory transpiled code!

115 |
116 |
117 |
118 |

Execution metrics!

119 |
120 |

Console output lurks here.

121 |

122 |             
123 |
124 |

Flow analysis!

125 |
126 |
127 |
128 |
129 |
130 |

Aether runs in node and the browser using node-style CommonJS requires.

131 |

In the browser, grab build/aether.js or build/aether.min.js. Or in node, runnpm install aether --save, which should add an entry like "aether": "~0.1.0" to your dependencies in package.json. Then, in your code:

132 |
var Aether = require('aether');
133 | var aether = new Aether();
134 | aether.lint(someGnarlyUserCode);
135 | aether.problems.warnings;
136 | [{range: [{ofs: 15, row: 2, col: 0}, {ofs: 22, row: 2, col: 7}],
137 |   id: 'aether_MissingThis',
138 |   message: 'Missing `this.` keyword; should be `this.getEnemies`.',
139 |   hint: 'There is no function `getEnemys`, but `this` has a method `getEnemies`.',
140 |   level: "warning"}]
141 | aether.transpile(someGnarlyUserCode);
142 | var gnarlyFunc = aether.createFunction();
143 | gnarlyFunc();
144 | // At any point, you can check aether.problems, aether.style, aether.flow, aether.metrics, etc.
145 | // See more examples in the tests: https://github.com/codecombat/aether/tree/master/test
146 | 
147 |

In the browser, it currently depends on lodash (not underscore). We test in Chrome and node, and it will probably work in other modern browsers.

148 |
149 |
150 |

Want to hack on Aether? Awesome!

151 |

Setting Up Your Environment

152 |

The GitHub Repository has everything you need to work on Aether. You'll need Node, NPM and Git installed.

153 |
    154 |
  1. Clone the repository: 155 |
    git clone https://github.com/codecombat/aether.git
    156 |
  2. 157 |
  3. Go to the directory and npm install: 158 |
    cd aether
    159 | npm install
    160 |
  4. 161 |
162 |

Running The Environment

163 |

From the aether directory, run:

164 |
grunt  # or grunt watch, to recompile on any changes
165 |

Then you can navigate to file://localhost/some/path/to/aether/build/index.html and see it in action.

166 |

Things to keep in mind

167 |
    168 |
  • Edit the CoffeeScript source in /src and /test, not the transpiled JavaScript in aether/lib.
  • 169 |
  • Remember to create tests in the /test folder.
  • 170 |
171 |

What to work on?

172 |

TODO

173 |

You could always also add more tests or handle reported issues.

174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | 182 | -------------------------------------------------------------------------------- /dev/demo.jade: -------------------------------------------------------------------------------- 1 | p.lead.text-center 2 | | Load example: 3 | select#example-select 4 | .row.demo-row 5 | #worst-problem-wrapper 6 | .col-md-4 7 | p.lead Write some JavaScript here. 8 | #javascript-input.ace-editor-wrapper.dev-section 9 | .col-md-4 10 | p.lead Use Aether on it. 11 | #aether-input.ace-editor-wrapper.dev-section 12 | .col-md-4 13 | p.lead Problems detected show up here. 14 | #aether-problems.dev-section 15 | .row.demo-row 16 | .col-md-4 17 | p.lead Gory transpiled code! 18 | #aether-input.ace-editor-wrapper.dev-section 19 | .col-md-4 20 | p.lead Execution metrics! 21 | #aether-metrics.dev-section 22 | p.lead Console output lurks here. 23 | pre#aether-console. 24 | 25 | .col-md-4 26 | p.lead Flow analysis! 27 | #aether-flow.dev-section 28 | -------------------------------------------------------------------------------- /dev/develop.jade: -------------------------------------------------------------------------------- 1 | p.lead Want to hack on Aether? Awesome! 2 | h3 Setting Up Your Environment 3 | p 4 | | The 5 | a(href='https://github.com/codecombat/aether') GitHub Repository 6 | | has 7 | strong everything 8 | | you need to work on Aether. You'll need 9 | a(href='http://www.joyent.com/blog/installing-node-and-npm') Node, NPM 10 | | and 11 | a(href='http://git-scm.com/') Git 12 | | installed. 13 | ol 14 | li 15 | strong Clone the repository 16 | | : 17 | pre. 18 | git clone https://github.com/codecombat/aether.git 19 | li 20 | | Go to the directory and 21 | strong npm install 22 | | : 23 | pre. 24 | cd aether 25 | npm install 26 | h3 Running The Environment 27 | p 28 | | From the aether directory, run: 29 | pre. 30 | grunt # or grunt watch, to recompile on any changes 31 | p 32 | | Then you can navigate to 33 | code file://localhost/some/path/to/aether/build/index.html 34 | | and see it in action. 35 | h3 Things to keep in mind 36 | ul 37 | li 38 | | Edit the CoffeeScript source in /src and /test, not the transpiled JavaScript in aether/lib. 39 | li Remember to create tests in the /test folder. 40 | h3 What to work on? 41 | p.strong TODO 42 | p 43 | | You could always also add more tests or handle 44 | a(href='https://github.com/codecombat/aether/issues') reported issues 45 | | . 46 | -------------------------------------------------------------------------------- /dev/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Aether 5 | meta(charset='utf-8') 6 | script(src='http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js') 7 | script(src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js') 8 | script(src='http://cdnjs.cloudflare.com/ajax/libs/ace/1.1.01/ace.js') 9 | script(src='http://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/js/bootstrap.min.js') 10 | script(src='http://codecombat.github.io/treema/js/vendor/tv4.js') 11 | script(src="http://google-code-prettify.googlecode.com/svn/loader/run_prettify.js?langs=js") 12 | script(src='http://codecombat.github.io/treema/js/treema.js') 13 | script(src='javascript.js') 14 | script(src='python.js') 15 | script(src='coffeescript.js') 16 | script(src='lua.js') 17 | script(src='java.js') 18 | script(src='aether.js') 19 | script(src='dev/index.js') 20 | script(src="http://localhost:35729/livereload.js") 21 | link(rel='stylesheet', href='http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css') 22 | link(rel='stylesheet', href='http://netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css') 23 | link(rel='stylesheet', href='http://codecombat.github.io/treema/css/treema.css') 24 | link(rel='stylesheet', href='dev/index.css') 25 | 26 | body 27 | #content 28 | a(href='https://github.com/codecombat/aether') 29 | img(style='position: absolute; top: 0; right: 0; border: 0;', src='https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png', alt='Fork me on GitHub') 30 | h1 Aether - Purify Your Users' JavaScript 31 | ul.nav.nav-pills.navbar.navbar-default 32 | li.active 33 | a(href='#overview', data-toggle='tab') Overview 34 | li 35 | a(href='#demo', data-toggle='tab') Demo 36 | li 37 | a(href='#usage', data-toggle='tab') Usage 38 | li 39 | a(href='#develop', data-toggle='tab') Dev 40 | li#github-link 41 | a(href='https://github.com/codecombat/aether') Aether on GitHub 42 | hr 43 | .tab-content 44 | #overview.tab-pane.fade.in.active 45 | include overview 46 | #demo.tab-pane.fade 47 | include demo 48 | #usage.tab-pane.fade 49 | include usage 50 | #develop.tab-pane.fade 51 | include develop 52 | footer 53 | div 54 | div 55 | -------------------------------------------------------------------------------- /dev/index.sass: -------------------------------------------------------------------------------- 1 | $bgcolor: rgba(33, 192, 251, 1) 2 | 3 | body 4 | background: $bgcolor url('http://i.imgur.com/uf36eRD.jpg') no-repeat center center fixed 5 | background-size: contain 6 | 7 | .nav-pills>li.active>a, .nav-pills>li.active>a:hover, .nav-pills>li.active>a:focus 8 | background-color: $bgcolor 9 | 10 | #content 11 | box-shadow: 0 0 6px 12 | margin: 0 auto 13 | width: 800px 14 | background: rgba(255, 255, 255, 0.95) 15 | padding: 20px 16 | 17 | h1 18 | border-radius: 3px 19 | padding: 15px 10px 20 | background: $bgcolor 21 | color: white 22 | text-shadow: 1px 1px 1px black 23 | margin-bottom: 0px 24 | 25 | .nav 26 | padding: 0 5px 27 | margin-top: 5px 28 | 29 | #github-link 30 | float: right 31 | background-color: rgba(0, 0, 0, 0.1) 32 | border: 1px solid rgba(0, 0, 0, 0.2) 33 | border-radius: 2px 34 | 35 | #demo h3 36 | margin-top: 40px 37 | 38 | h4 39 | margin: 20px 0 40 | 41 | li 42 | margin: 4px 0 43 | 44 | // Demos 45 | 46 | .tab-pane 47 | padding: 10px 48 | margin-bottom: 20px 49 | border-left: 1px solid lightgray 50 | border-right: 1px solid lightgray 51 | box-shadow: 0 0 10px #aaa 52 | 53 | .nav-tabs li 54 | z-index: 1 55 | 56 | pre 57 | &.tab-pane 58 | border: 0 59 | margin-bottom: 20px 60 | 61 | // Musical double bar footer 62 | 63 | footer 64 | margin-top: 50px 65 | text-align: center 66 | clear: both 67 | div 68 | background-color: #666 69 | .bar 70 | margin-top: 10px 71 | width: 100% 72 | div 73 | &:first-child 74 | height: 5px 75 | &:last-child 76 | height: 20px 77 | margin-top: 10px 78 | 79 | pre 80 | font-size: 12px 81 | 82 | #demo 83 | background: white 84 | position: absolute 85 | left: 20px 86 | right: 20px 87 | top: 180px 88 | .ace-editor-wrapper 89 | position: absolute 90 | top: 35px 91 | right: 20px 92 | bottom: 0 93 | left: 20px 94 | height: 380px 95 | 96 | #aether-problems, #aether-console, #aether-metrics, #aether-flow 97 | overflow: scroll 98 | max-height: 380px 99 | 100 | #demo 101 | #aether-metrics 102 | height: 200px 103 | #aether-console 104 | height: 100px 105 | 106 | .ace_editor .executed 107 | background-color: rgba(216, 88, 44, 0.125) 108 | position: absolute 109 | 110 | .demo-row 111 | height: 430px 112 | 113 | .dev-section 114 | background-color: #fafafa 115 | 116 | #worst-problem-wrapper 117 | position: absolute 118 | z-index: 100 119 | top: 445px 120 | left: 60px 121 | font-size: 20px 122 | background-color: rgba(240, 240, 255, 0.8) 123 | &.error 124 | color: red 125 | &.warning 126 | color: orange 127 | &.info 128 | color: green 129 | -------------------------------------------------------------------------------- /dev/overview.jade: -------------------------------------------------------------------------------- 1 | p.lead 2 | strong Aether 3 | | aims to make it easy for people to learn and write JavaScript and CoffeeScript by helping them catch and fix bugs, 4 | | letting them see and more easily understand the execution of their program 5 | a(href='http://youtu.be/PUv66718DII?t=17m25s') (like Bret Victor commanded!) 6 | | , and giving them tips on how they can improve their code. 7 | a(href='http://codecombat.com/') CodeCombat 8 | | is behind it. 9 | hr 10 | h2 11 | a.anchor(name='get-in-touch', href='#get-in-touch') 12 | | Get in touch 13 | p 14 | | You can use the 15 | a(href='https://github.com/codecombat/aether/issues') GitHub issues 16 | | , the 17 | a(href='http://discourse.codecombat.com/') Discourse forum 18 | | , the 19 | a(href='https://groups.google.com/forum/#!forum/aether-dev') Google Group 20 | | , the 21 | a(href='http://www.hipchat.com/g3plnOKqa') HipChat 22 | | , or 23 | a(href='mailto:nick@codecombat.com') email 24 | | 25 | a(href='http://www.nickwinter.net/') Nick 26 | | . 27 | h2 28 | a.anchor(name='what-is-it', href='#what-is-it') 29 | | What is it? 30 | p 31 | | It's a JavaScript library (written in CoffeeScript) that takes user code as input; does computer-sciencey transpilation things to it with the help of 32 | a(href='http://jshint.com/') JSHint 33 | | , 34 | a(href='http://esprima.org/') Esprima 35 | | , 36 | a(href='https://github.com/wala/JS_WALA') JS_WALA 37 | | , 38 | a(href='https://github.com/Constellation/escodegen') escodegen 39 | | , and 40 | a(href='https://github.com/google/traceur-compiler') traceur 41 | | ; and gives you: 42 | ol 43 | li 44 | p 45 | em incomplete 46 | | 47 | strong Linting 48 | | with totally configurable error/warning/info messages. Instead of 49 | code "Warning: Line 2: Missing semicolon." 50 | | , you could make Aether say, 51 | code "Error: Put a semicolon here on line 2 after 'this.explode()' to end the statement." 52 | | , or 53 | code "fyi: might want a ; here amiga" 54 | | , or nothing at all. Aether does spellchecking based on what properties should exist, too: 55 | code Should be 'destroy', not 'destory'. 56 | li 57 | p 58 | strong Transformation 59 | | like with 60 | a(href='https://github.com/substack/node-falafel') node-falafel 61 | | , but even easier, because your source transformation plugins can run either before or after the 62 | a(href='http://en.wikipedia.org/wiki/Abstract_syntax_tree') AST 63 | | is normalized with 64 | a(href='https://github.com/wala/JS_WALA') JS_WALA 65 | | . 66 | li 67 | p 68 | em incomplete 69 | | 70 | strong Sandboxing 71 | | so that you can actually run user code in the browser without worrying if the world will end. Well, not actually. 72 | a(href='http://www.adsafe.org/') That 73 | | is 74 | a(href='http://seclab.stanford.edu/websec/jsPapers/w2sp.pdf') hard 75 | | ; one should at least use a web worker. But good enough to foil most hacking attempts. 76 | li 77 | p 78 | strong Instrumentation 79 | | so that when the code is run, you'll know everything that happened. Count statements executed, step through the flow of execution, retrieve past variable states, and even pause and resume execution whenever you want. Pause after every statement? Sure! 80 | li 81 | p 82 | em incomplete 83 | | 84 | strong 85 | a(href='https://github.com/codecombat/aether/issues/2') Time-travel debugging 86 | | to combine and surpass the best parts of both stepping debugging and logging. 87 | li 88 | p 89 | em planned 90 | | 91 | strong Style analysis 92 | | : metrics on things like what kind of indentation and bracket style the code uses, whether it uses recursion or regular expressions, etc. 93 | li 94 | p 95 | em planned 96 | | 97 | strong Autocompletion 98 | | suggestions based on unfinished code. 99 | li 100 | p 101 | em planned 102 | | 103 | strong Other goodies 104 | | ! You can let your users code in 105 | a(href='http://www.slideshare.net/domenicdenicola/es6-the-awesome-parts') ES6 106 | | now and hopefully 107 | a(href='https://github.com/michaelficarra/CoffeeScriptRedux') CoffeeScript 108 | | soon. 109 | h3 110 | a.anchor(name='development-status', href='#development-status') 111 | | Development Status 112 | p 113 | | Alpha–watch out! 114 | a(href='http://codecombat.com/') CodeCombat 115 | | is using it right now, but there are many bugs and missing pieces. If you'd be interested in Aether were it solid and finished, 116 | em please 117 | | tell us so we can get an idea of where else it might be useful and how it should work. 118 | 119 | h3 How does it work? 120 | p Aether uses JSHint, Esprima, Acorn, JS_WALA, escodegen, and traceur together to carry out this process: 121 | ol 122 | li We use JSHint to provide lint warnings and errors. 123 | li We wrap the user code in a function declaration, since Aether expects the user code to be the body of a function. 124 | li We do a regexp replace step to check for common mistakes. This will probably go away. 125 | li We parse it with Esprima to get an AST. If it's invalid, we fall back to parsing with Acorn in loose mode so that we can get a workable AST despite the errors. 126 | li We run a bunch of pre-normalization transformations on the AST to grab variable declarations, original statement ranges, and check for several more types of mistakes. 127 | li We use JS_WALA to normalize the AST so that there'll be far fewer cases to handle in the next transformation step. 128 | li We output that transformed AST to JS again using escodegen, since our transformations need to operate on an AST with matching original source at the same time. 129 | li We parse again with Esprima and run a bunch of post-normalization transformations on that AST to do things like inserting instrumentation, protecting external objects, and adding yield statements. 130 | li We use traceur to convert our ES6 yield statements to giant ES5 state machines to simulate generators, if we added any yields. 131 | li 132 | | We add one more function to intercept references to 133 | code this 134 | | for security. 135 | 136 | h3 License 137 | 138 | p.strong 139 | a(href="https://github.com/codecombat/aether/blob/master/LICENSE") The MIT License (MIT) 140 | 141 | p 142 | | If you'd like to contribute, please 143 | a(href="http://codecombat.com/cla") sign the CodeCombat contributor license agreement 144 | | so we can accept your pull requests. It is easy. 145 | -------------------------------------------------------------------------------- /dev/usage.jade: -------------------------------------------------------------------------------- 1 | p.lead 2 | | Aether runs in node and the browser using 3 | a(href='http://addyosmani.com/writing-modular-js/') node-style CommonJS requires 4 | | . 5 | p 6 | | In the browser, grab 7 | a(href='https://github.com/codecombat/aether/blob/master/build/aether.js') build/aether.js 8 | | or 9 | a(href='https://github.com/codecombat/aether/blob/master/build/aether.min.js') build/aether.min.js 10 | | . Or in node, run 11 | code npm install aether --save 12 | | , which should add an entry like 13 | code "aether": "~0.1.0" 14 | | to your 15 | code dependencies 16 | | in 17 | code package.json 18 | | . Then, in your code: 19 | pre.prettyprint. 20 | var Aether = require('aether'); 21 | var aether = new Aether(); 22 | aether.lint(someGnarlyUserCode); 23 | aether.problems.warnings; 24 | [{range: [{ofs: 15, row: 2, col: 0}, {ofs: 22, row: 2, col: 7}], 25 | id: 'aether_MissingThis', 26 | message: 'Missing `this.` keyword; should be `this.getEnemies`.', 27 | hint: 'There is no function `getEnemys`, but `this` has a method `getEnemies`.', 28 | level: "warning"}] 29 | aether.transpile(someGnarlyUserCode); 30 | var gnarlyFunc = aether.createFunction(); 31 | gnarlyFunc(); 32 | // At any point, you can check aether.problems, aether.style, aether.flow, aether.metrics, etc. 33 | // See more examples in the tests: https://github.com/codecombat/aether/tree/master/test 34 | 35 | p 36 | | In the browser, it currently depends on 37 | a(href='http://lodash.com/') lodash 38 | | (not underscore). We test in Chrome and node, and it will probably work in other modern browsers. 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aether", 3 | "version": "0.5.40", 4 | "description": "Analyzes, instruments, and transpiles JS to help beginners.", 5 | "keywords": [ 6 | "lint", 7 | "static analysis", 8 | "transpiler", 9 | "learning programming" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/codecombat/aether" 14 | }, 15 | "contributors": [ 16 | "Nick Winter (http://www.nickwinter.net/)", 17 | "Michael Schmatz (https://github.com/schmatz)", 18 | "Scott Erickson (https://github.com/sderickson)" 19 | ], 20 | "main": "lib/aether", 21 | "dependencies": { 22 | "acorn": "~0.3.1", 23 | "aether-lang-stdlibs": "^1.0.1", 24 | "cashew-js": "git://github.com/codecombat/cashew", 25 | "coffee-script-redux": "git://github.com/michaelficarra/CoffeeScriptRedux", 26 | "deku": "^2.0.0-rc16", 27 | "escodegen": "~1.3.0", 28 | "esper.js": "^0.1.0", 29 | "esprima": "^2.7.1", 30 | "estraverse": "~1.5.0", 31 | "skulpty": "git://github.com/codecombat/skulpty", 32 | "htmlparser2": "^3.9.1", 33 | "jshint": "~2.3.0", 34 | "lodash": "~2.4.1", 35 | "lua2js": "~0.0.7", 36 | "string_score": "^0.1.20", 37 | "tv4": "~1.0.7" 38 | }, 39 | "devDependencies": { 40 | "coffeelint": "^1.13.0", 41 | "grunt": "~0.4.1", 42 | "grunt-browserify": "~2.1.0", 43 | "grunt-coffeelint": "~0.0.7", 44 | "grunt-contrib-coffee": "~0.7.0", 45 | "grunt-contrib-copy": "^0.5.0", 46 | "grunt-contrib-jade": "~0.9.1", 47 | "grunt-contrib-jasmine": "~0.5.1", 48 | "grunt-contrib-sass": "~0.7.2", 49 | "grunt-contrib-uglify": "~0.2.2", 50 | "grunt-contrib-watch": "~0.5.1", 51 | "grunt-gh-pages": "~0.9.0", 52 | "grunt-istanbul": "^0.7.0", 53 | "grunt-jasmine-node": "~0.1.0", 54 | "grunt-newer": "^1.1.0", 55 | "grunt-notify": "~0.2.7", 56 | "grunt-push-release": "~0.1.1", 57 | "grunt-string-replace": "^1.2.0", 58 | "jasmine-node": "~1.11.0" 59 | }, 60 | "scripts": { 61 | "test": "grunt travis --verbose" 62 | }, 63 | "licenses": [ 64 | { 65 | "type": "MIT", 66 | "url": "https://github.com/nwinter/aether/blob/master/LICENSE" 67 | } 68 | ], 69 | "engines": { 70 | "node": ">=0.8" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /parsers/coffeescript.js: -------------------------------------------------------------------------------- 1 | window.aetherCoffeeScriptRedux = require('coffee-script-redux'); 2 | -------------------------------------------------------------------------------- /parsers/html.js: -------------------------------------------------------------------------------- 1 | // put Deku and HTMLParser2 up in there 2 | window.deku = require('deku'); 3 | window.htmlparser2 = require('htmlparser2'); 4 | -------------------------------------------------------------------------------- /parsers/java.js: -------------------------------------------------------------------------------- 1 | window.aetherCashew = require('cashew-js'); -------------------------------------------------------------------------------- /parsers/javascript.js: -------------------------------------------------------------------------------- 1 | window.aetherJSHint = require('jshint'); 2 | -------------------------------------------------------------------------------- /parsers/lua.js: -------------------------------------------------------------------------------- 1 | window.aetherLua2JS = require('lua2js'); 2 | -------------------------------------------------------------------------------- /parsers/python.js: -------------------------------------------------------------------------------- 1 | window.aetherFilbert = require('skulpty'); 2 | window.aetherFilbertLoose = require('skulpty'); 3 | -------------------------------------------------------------------------------- /rt.coffee: -------------------------------------------------------------------------------- 1 | Aether = require './src/aether' 2 | 3 | aether = new Aether 4 | language: "java" 5 | executionLimit: 1000, 6 | problems: { 7 | jshint_W040: {level: "ignore"}, 8 | aether_MissingThis: {level: "warning"} 9 | }, 10 | yieldConditionally: true 11 | language: 'java' 12 | includeFlow: false 13 | includeMetrics: false 14 | 15 | aether.className = "One" 16 | aether.staticCall = "main" 17 | 18 | code = """ 19 | public class One { 20 | public static void main(String[] arg) { 21 | hero.charge(); 22 | hero.hesitate(); 23 | hero.hesitate(); 24 | hero.charge(); 25 | hero.hesitate(); 26 | hero.charge(); 27 | } 28 | } 29 | """ 30 | 31 | aether.transpile code 32 | console.log "A", aether.problems 33 | 34 | thisValue = 35 | charge: () -> 36 | @say "attack!" 37 | return "attack!"; 38 | hesitate: () -> 39 | @say "uhh..." 40 | aether._shouldYield = true 41 | say: console.log 42 | 43 | 44 | method = aether.createMethod thisValue 45 | generator = method() 46 | aether.sandboxGenerator generator 47 | 48 | executeSomeMore = () -> 49 | result = generator.next() 50 | if not result.done 51 | setTimeout executeSomeMore, 2000 52 | 53 | executeSomeMore() 54 | 55 | -------------------------------------------------------------------------------- /sc.coffee: -------------------------------------------------------------------------------- 1 | Aether = require './src/aether' 2 | 3 | aether = new Aether 4 | language: "java" 5 | executionLimit: 1000, 6 | problems: { 7 | jshint_W040: {level: "ignore"}, 8 | aether_MissingThis: {level: "warning"} 9 | }, 10 | yieldConditionally: true 11 | language: 'lua' 12 | includeFlow: false 13 | includeMetrics: false 14 | 15 | aether.className = "One" 16 | aether.staticCall = "main" 17 | 18 | code = """ 19 | function doit() 20 | self.charge() 21 | self.hesitate() 22 | end 23 | 24 | self.hesitate() 25 | self.charge() 26 | self.hesitate() 27 | self.charge() 28 | """ 29 | 30 | aether.transpile code 31 | console.log "A", aether.problems 32 | console.log "C", aether.getStatementCount() 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/aether.coffee: -------------------------------------------------------------------------------- 1 | self = window if window? and not self? 2 | self = global if global? and not self? 3 | self.self ?= self 4 | 5 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 6 | 7 | esprima = require 'esprima' # getting our Esprima Harmony 8 | 9 | defaults = require './defaults' 10 | problems = require './problems' 11 | execution = require './execution' 12 | traversal = require './traversal' 13 | transforms = require './transforms' 14 | protectBuiltins = require './protectBuiltins' 15 | optionsValidator = require './validators/options' 16 | languages = require './languages/languages' 17 | interpreter = require './interpreter' 18 | 19 | module.exports = class Aether 20 | @execution: execution 21 | @addGlobal: protectBuiltins.addGlobal # Call instance method version after instance creation to update existing global list 22 | @replaceBuiltin: protectBuiltins.replaceBuiltin 23 | @globals: protectBuiltins.addedGlobals 24 | 25 | # Current call depth 26 | depth: 0 27 | 28 | getAddedGlobals: () -> 29 | protectBuiltins.addedGlobals 30 | 31 | addGlobal: (name, value) -> 32 | # Call class method version before instance creation to instantiate global list 33 | if @esperEngine? 34 | @esperEngine.addGlobal name, value 35 | 36 | constructor: (options) -> 37 | options ?= {} 38 | validationResults = optionsValidator options 39 | unless validationResults.valid 40 | throw new Error "Aether options are invalid: " + JSON.stringify(validationResults.errors, null, 4) 41 | 42 | # Save our original options for recreating this Aether later. 43 | @originalOptions = _.cloneDeep options # TODO: slow 44 | 45 | # Merge the given options with the defaults. 46 | defaultsCopy = _.cloneDeep defaults 47 | @options = _.merge defaultsCopy, options 48 | 49 | @setLanguage @options.language 50 | @allGlobals = @options.globals.concat protectBuiltins.builtinNames, Object.keys(@language.runtimeGlobals) # After setLanguage, which can add globals. 51 | #if statementStack[0]? 52 | # rng = statementStack[0].originalRange 53 | # aether.lastStatementRange = [rng.start, rng.end] if rng 54 | 55 | Object.defineProperty @, 'lastStatementRange', 56 | get: () -> 57 | rng = @esperEngine?.evaluator?.lastASTNodeProcessed?.originalRange 58 | return [rng.start, rng.end] if rng 59 | 60 | # Language can be changed after construction. (It will reset Aether's state.) 61 | setLanguage: (language) -> 62 | return if @language and @language.id is language 63 | validationResults = optionsValidator language: language 64 | unless validationResults.valid 65 | throw new Error "New language is invalid: " + JSON.stringify(validationResults.errors, null, 4) 66 | @originalOptions.language = @options.language = language 67 | @language = new languages[language]() 68 | @languageJS ?= if language is 'javascript' then @language else new languages.javascript 'ES5' 69 | @reset() 70 | return language 71 | 72 | # Resets the state of Aether, readying it for a fresh transpile. 73 | reset: -> 74 | @problems = errors: [], warnings: [], infos: [] 75 | @style = {} 76 | @flow = {} 77 | @metrics = {} 78 | @pure = null 79 | 80 | # Convert to JSON so we can pass it across web workers and HTTP requests and store it in databases and such. 81 | serialize: -> 82 | _.pick @, ['originalOptions', 'raw', 'pure', 'problems', 'flow', 'metrics', 'style', 'ast'] 83 | 84 | # Convert a serialized Aether instance back from JSON. 85 | @deserialize: (serialized) -> 86 | aether = new Aether serialized.originalOptions 87 | aether[prop] = val for prop, val of serialized when prop isnt "originalOptions" 88 | aether 89 | 90 | # Performs quick heuristics to determine whether the code will run or produce compilation errors. 91 | # If thorough, it will perform detailed linting and return false if there are any lint errors. 92 | canTranspile: (rawCode, thorough=false) -> 93 | return true if not rawCode # blank code should compile, but bypass the other steps 94 | return false if @language.obviouslyCannotTranspile rawCode 95 | return true unless thorough 96 | @lint(rawCode, @).errors.length is 0 97 | 98 | # Determine whether two strings of code are significantly different. 99 | # If careAboutLineNumbers, we strip trailing comments and whitespace and compare line count. 100 | # If careAboutLint, we also lint and make sure lint problems are the same. 101 | hasChangedSignificantly: (a, b, careAboutLineNumbers=false, careAboutLint=false) -> 102 | return true unless a? and b? 103 | return false if a is b 104 | return true if careAboutLineNumbers and @language.hasChangedLineNumbers a, b 105 | return true if careAboutLint and @hasChangedLintProblems a, b 106 | # If the simple tests fail, we compare abstract syntax trees for equality. 107 | @language.hasChangedASTs a, b 108 | 109 | # Determine whether two strings of code produce different lint problems. 110 | hasChangedLintProblems: (a, b) -> 111 | aLintProblems = ([p.id, p.message, p.hint] for p in @getAllProblems @lint a) 112 | bLintProblems = ([p.id, p.message, p.hint] for p in @getAllProblems @lint b) 113 | return not _.isEqual aLintProblems, bLintProblems 114 | 115 | # Return a beautified representation of the code (cleaning up indentation, etc.) 116 | beautify: (rawCode) -> 117 | @language.beautify rawCode, @ 118 | 119 | # Transpile it. Even if it can't transpile, it will give syntax errors and warnings and such. Clears any old state. 120 | transpile: (@raw) -> 121 | @reset() 122 | rawCode = @raw 123 | @problems = @lint rawCode 124 | @pure = @purifyCode rawCode 125 | @pure 126 | 127 | # Perform some fast static analysis (without transpiling) and find any lint problems. 128 | lint: (rawCode) -> 129 | lintProblems = errors: [], warnings: [], infos: [] 130 | @addProblem problem, lintProblems for problem in @language.lint rawCode, @ 131 | lintProblems 132 | 133 | # Return a ready-to-interpret function from the parsed code. 134 | createFunction: -> 135 | return interpreter.createFunction @ 136 | 137 | # Like createFunction, but binds method to thisValue. 138 | createMethod: (thisValue) -> 139 | _.bind @createFunction(), thisValue 140 | 141 | # Convenience wrapper for running the compiled function with default error handling 142 | run: (fn, args...) -> 143 | try 144 | fn ?= @createFunction() 145 | catch error 146 | problem = @createUserCodeProblem error: error, code: @raw, type: 'transpile', reporter: 'aether' 147 | @addProblem problem 148 | return 149 | try 150 | fn args... 151 | catch error 152 | problem = @createUserCodeProblem error: error, code: @raw, type: 'runtime', reporter: 'aether' 153 | @addProblem problem 154 | return 155 | 156 | # Create a standard Aether problem object out of some sort of transpile or runtime problem. 157 | createUserCodeProblem: problems.createUserCodeProblem 158 | 159 | createThread: (fx) -> 160 | interpreter.createThread @, fx 161 | 162 | updateProblemContext: (problemContext) -> 163 | @options.problemContext = problemContext 164 | 165 | # Add problem to the proper level's array within the given problems object (or @problems). 166 | addProblem: (problem, problems=null) -> 167 | return if problem.level is "ignore" 168 | (problems ? @problems)[problem.level + "s"].push problem 169 | problem 170 | 171 | # Return all the problems as a flat array. 172 | getAllProblems: (problems) -> 173 | _.flatten _.values (problems ? @problems) 174 | 175 | # The meat of the transpilation. 176 | purifyCode: (rawCode) -> 177 | preprocessedCode = @language.hackCommonMistakes rawCode, @ # TODO: if we could somehow not change the source ranges here, that would be awesome.... but we'll probably just need to get rid of this step. 178 | wrappedCode = @language.wrap preprocessedCode, @ 179 | 180 | originalNodeRanges = [] 181 | varNames = {} 182 | varNames[parameter] = true for parameter in @options.functionParameters 183 | preNormalizationTransforms = [ 184 | transforms.makeGatherNodeRanges originalNodeRanges, wrappedCode, @language.wrappedCodePrefix 185 | transforms.makeCheckThisKeywords @allGlobals, varNames, @language, @options.problemContext 186 | transforms.makeCheckIncompleteMembers @language, @options.problemContext 187 | ] 188 | try 189 | [transformedCode, transformedAST] = @transform wrappedCode, preNormalizationTransforms, @language.parse 190 | @ast = transformedAST 191 | catch error 192 | problemOptions = error: error, code: wrappedCode, codePrefix: @language.wrappedCodePrefix, reporter: @language.parserID, kind: error.index or error.id, type: 'transpile' 193 | @addProblem @createUserCodeProblem problemOptions 194 | return '' unless @language.parseDammit 195 | originalNodeRanges.splice() # Reset any ranges we did find; we'll try again. 196 | try 197 | [transformedCode, transformedAST] = @transform wrappedCode, preNormalizationTransforms, @language.parseDammit 198 | @ast = transformedAST 199 | catch error 200 | problemOptions.kind = error.index or error.id 201 | problemOptions.reporter = 'acorn_loose' if @language.id is 'javascript' 202 | @addProblem @createUserCodeProblem problemOptions 203 | return '' 204 | 205 | # Now we've shed all the trappings of the original language behind; it's just JavaScript from here on. 206 | nodeGatherer = transforms.makeGatherNodeRanges originalNodeRanges, wrappedCode, @language.wrappedCodePrefix 207 | 208 | traversal.walkASTCorrect @ast, (node) => 209 | nodeGatherer(node) 210 | if node.originalRange? 211 | startEndRangeArray = @language.removeWrappedIndent [node.originalRange.start, node.originalRange.end] 212 | node.originalRange = 213 | start: startEndRangeArray[0] 214 | end: startEndRangeArray[1] 215 | 216 | # TODO: return nothing, or the AST, and make sure CodeCombat can handle it returning nothing 217 | return rawCode 218 | 219 | transform: (code, transforms, parseFn) -> 220 | transformedCode = traversal.morphAST code, (_.bind t, @ for t in transforms), parseFn, @ 221 | transformedAST = parseFn transformedCode, @ 222 | [transformedCode, transformedAST] 223 | 224 | @getFunctionBody: (func) -> 225 | # Remove function() { ... } wrapper and any extra indentation 226 | source = if _.isString func then func else func.toString() 227 | return "" if source.trim() is "function () {}" 228 | source = source.substring(source.indexOf('{') + 2, source.lastIndexOf('}')) #.trim() 229 | lines = source.split /\r?\n/ 230 | indent = if lines.length then lines[0].length - lines[0].replace(/^ +/, '').length else 0 231 | (line.slice indent for line in lines).join '\n' 232 | 233 | convertToNativeType: (obj) -> 234 | # Convert obj to current language's equivalent type if necessary 235 | # E.g. if language is Python, JavaScript Array is converted to a Python list 236 | @language.convertToNativeType(obj) 237 | 238 | getStatementCount: -> 239 | count = 0 240 | if @language.usesFunctionWrapping() 241 | root = @ast.body[0].body # We assume the 'code' is one function hanging inside the program. 242 | else 243 | root = @ast.body 244 | 245 | #console.log(JSON.stringify root, null, ' ') 246 | traversal.walkASTCorrect root, (node) -> 247 | return if not node.type? 248 | return if node.userCode == false 249 | if node.type in [ 250 | 'ExpressionStatement', 'ReturnStatement', 'ForStatement', 'ForInStatement', 251 | 'WhileStatement', 'DoWhileStatement', 'FunctionDeclaration', 'VariableDeclaration', 252 | 'IfStatement', 'SwitchStatement', 'ThrowStatement', 'ContinueStatement', 'BreakStatement' 253 | ] 254 | ++count 255 | return count 256 | 257 | self.Aether = Aether if self? 258 | window.Aether = Aether if window? 259 | self.esprima ?= esprima if self? 260 | window.esprima ?= esprima if window? 261 | -------------------------------------------------------------------------------- /src/defaults.coffee: -------------------------------------------------------------------------------- 1 | execution = require './execution' 2 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 3 | 4 | module.exports = defaults = 5 | thisValue: null # TODO: don't use this. Aether doesn't use it at compile time and CodeCombat uses it just at runtime, and it makes cloning original options weird/unintuitive/slow. 6 | globals: [] 7 | language: "javascript" 8 | functionName: null # In case we need it for error messages 9 | functionParameters: [] # Or something like ["target"] 10 | yieldAutomatically: false # Horrible name... we could have it auto-insert yields after every statement 11 | yieldConditionally: false # Also bad name, but what it would do is make it yield whenever this._aetherShouldYield is true (and clear it) 12 | executionCosts: {} # execution # We don't use this yet 13 | noSerializationInFlow: false 14 | noVariablesInFlow: false 15 | skipDuplicateUserInfoInFlow: false 16 | includeFlow: true 17 | includeMetrics: true 18 | includeStyle: true 19 | protectBuiltins: true 20 | protectAPI: false 21 | debug: false 22 | -------------------------------------------------------------------------------- /src/execution.coffee: -------------------------------------------------------------------------------- 1 | #esprima = require 'esprima' 2 | #Syntax = esprima.Syntax 3 | 4 | module.exports = execution = 5 | # Based on Esprima Harmony's Syntax map for Mozilla's Parser AST 6 | # https://github.com/ariya/esprima/blob/harmony/esprima.js#L118 7 | ArrayExpression: 1 8 | ArrayPattern: 1 9 | ArrowFunctionExpression: 1 10 | AssignmentExpression: 1 11 | BinaryExpression: 1 12 | BlockStatement: 1 13 | BreakStatement: 1 14 | CallExpression: 1 15 | CatchClause: 1 16 | ClassBody: 1 17 | ClassDeclaration: 1 18 | ClassExpression: 1 19 | ClassHeritage: 1 20 | ComprehensionBlock: 1 21 | ComprehensionExpression: 1 22 | ConditionalExpression: 1 23 | ContinueStatement: 1 24 | DebuggerStatement: 1 25 | DoWhileStatement: 1 26 | EmptyStatement: 1 27 | ExportDeclaration: 1 28 | ExportBatchSpecifier: 1 29 | ExportSpecifier: 1 30 | ExpressionStatement: 1 31 | ForInStatement: 1 32 | ForOfStatement: 1 33 | ForStatement: 1 34 | FunctionDeclaration: 1 35 | FunctionExpression: 1 36 | Identifier: 1 37 | IfStatement: 1 38 | ImportDeclaration: 1 39 | ImportSpecifier: 1 40 | LabeledStatement: 1 41 | Literal: 1 42 | LogicalExpression: 1 43 | MemberExpression: 1 44 | MethodDefinition: 1 45 | ModuleDeclaration: 1 46 | NewExpression: 1 47 | ObjectExpression: 1 48 | ObjectPattern: 1 49 | Program: 1 50 | Property: 1 51 | ReturnStatement: 1 52 | SequenceExpression: 1 53 | SpreadElement: 1 54 | SwitchCase: 1 55 | SwitchStatement: 1 56 | TaggedTemplateExpression: 1 57 | TemplateElement: 1 58 | TemplateLiteral: 1 59 | ThisExpression: 1 60 | ThrowStatement: 1 61 | TryStatement: 1 62 | UnaryExpression: 1 63 | UpdateExpression: 1 64 | VariableDeclaration: 1 65 | VariableDeclarator: 1 66 | WhileStatement: 1 67 | WithStatement: 1 68 | YieldExpression: 1 69 | 70 | # What about custom execution costs for different operators and functions and other things? -------------------------------------------------------------------------------- /src/interpreter.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' 2 | addedGlobals = require('./protectBuiltins').addedGlobals 3 | 4 | isStatement = (name) -> 5 | name not in [ 6 | 'Literal', 'Identifier', 'ThisExpression', 'BlockStatement', 'MemberExpression', 7 | 'FunctionExpression', 'LogicalExpression', 'BinaryExpression', 'UnaryExpression', 8 | 'Program' 9 | ] 10 | 11 | shouldFlow = (name) -> 12 | name not in [ 13 | 'IfStatement', 'WhileStatement', 'DoWhileStatement', 'ForStatement', 'ForInStatement', 'ForOfStatement' 14 | ] 15 | 16 | updateState = (aether, evaluator) -> 17 | frame_stack = evaluator.frames 18 | top = frame_stack[0] 19 | bottom = frame_stack[frame_stack.length - 1] 20 | 21 | if aether.options.includeFlow 22 | unless bottom.flow? 23 | bottom.flow = {statementsExecuted: 0, statements: []} 24 | aether.flow.states ?= [] 25 | aether.flow.states.push bottom.flow 26 | 27 | if aether.options.includeMetrics 28 | aether.metrics.statementsExecuted ?= 0 29 | aether.metrics.callsExecuted ?= 0 30 | 31 | astStack = (x.ast for x in frame_stack when x.ast?) 32 | statementStack = ( x for x in astStack when isStatement x.type ) 33 | 34 | if top.ast? 35 | ++aether.metrics.callsExecuted if aether.options.includeMetrics and top.ast.type == 'CallExpression' 36 | 37 | if isStatement top.ast.type 38 | ++aether.metrics.statementsExecuted if aether.options.includeMetrics 39 | ++bottom.flow.statementsExecuted if bottom.flow? 40 | 41 | if bottom.flow? and shouldFlow(top.ast.type) 42 | f = {} 43 | f.userInfo = _.cloneDeep aether._userInfo if aether._userInfo? 44 | unless aether.options.noVariablesInFlow 45 | variables = {} 46 | for s in [(frame_stack.length - 2) .. 0] 47 | p = frame_stack[s] 48 | continue unless p and p.scope 49 | for n in Object.keys(p.scope.object.properties) 50 | continue if n[0] is '_' 51 | variables[n] = p.value.debugString if p.value 52 | f.variables = variables 53 | 54 | rng = top.ast.originalRange 55 | f.range = [rng.start, rng.end] if rng 56 | f.type = top.ast.type 57 | 58 | bottom.flow.statements.push f unless not f.range # Dont push statements without ranges 59 | 60 | #module.exports.parse = (aether, code) -> 61 | # esper = window?.esper ? self?.esper ? global?.esper ? require 'esper.js' 62 | # esper.plugin 'lang-' + aether.language.id 63 | # return esper.languages[aether.language.id].parser(code, inFunctionBody: true) 64 | 65 | module.exports.createFunction = (aether) -> 66 | esper = window?.esper ? self?.esper ? global?.esper ? require 'esper.js' 67 | # TODO: set the language in Esper to let Esper use native language code 68 | #esper.plugin 'lang-' + aether.language.id 69 | state = {} 70 | #aether.flow.states.push state 71 | messWithLoops = false 72 | if aether.options.whileTrueAutoYield or aether.options.simpleLoops 73 | messWithLoops = true 74 | 75 | unless aether.esperEngine 76 | aether.esperEngine = new esper.Engine 77 | strict: aether.language.id not in ['python', 'lua'] 78 | foreignObjectMode: if aether.options.protectAPI then 'smart' else 'link' 79 | extraErrorInfo: true 80 | yieldPower: 2 81 | debug: aether.options.debug 82 | #language: aether.language.id # TODO: set the language in Esper to let Esper use native language code 83 | 84 | engine = aether.esperEngine 85 | #console.log JSON.stringify(aether.ast, null, ' ') 86 | 87 | #fxName = aether.ast.body[0].id.name 88 | fxName = aether.options.functionName or 'foo' 89 | #console.log JSON.stringify(aether.ast, null, " ") 90 | aether.language.setupInterpreter engine 91 | 92 | # TODO: remove this when setting language in Esper to let Esper use native language code 93 | if aether.language.injectCode? 94 | engine.evalASTSync(aether.language.injectCode, {nonUserCode: true}) 95 | else 96 | engine.evalSync('') #Force context to be created 97 | 98 | for name in Object.keys addedGlobals 99 | engine.addGlobal(name, addedGlobals[name]) 100 | 101 | upgradeEvaluator aether, engine.evaluator 102 | 103 | try 104 | # Only Coffeescript at this point. 105 | if aether.language.usesFunctionWrapping() 106 | engine.evalASTSync aether.ast 107 | if aether.options.yieldConditionally 108 | fx = engine.fetchFunction fxName, makeYieldFilter(aether) 109 | else if aether.options.yieldAutomatically 110 | fx = engine.fetchFunction fxName, (engine) -> true 111 | else 112 | fx = engine.fetchFunctionSync fxName 113 | else 114 | if aether.options.yieldConditionally 115 | fx = engine.functionFromAST aether.ast, makeYieldFilter(aether) 116 | else if aether.options.yieldAutomatically 117 | fx = engine.functionFromAST aether.ast, (engine) -> true 118 | else 119 | fx = engine.functionFromASTSync aether.ast 120 | catch error 121 | console.log 'Esper: error parsing AST. Returning empty function.', error.message 122 | if aether.language.id is 'javascript' 123 | error.message = "Couldn't understand your code. Are your { and } braces matched?" 124 | else 125 | error.message = "Couldn't understand your code. Do you have extra spaces at the beginning, or unmatched ( and ) parentheses?" 126 | aether.addProblem aether.createUserCodeProblem error: error, code: aether.raw, type: 'transpile', reporter: 'aether' 127 | engine.evalASTSync emptyAST 128 | #console.log require('escodegen').generate(aether.ast) 129 | 130 | 131 | 132 | 133 | return fx 134 | 135 | debugDumper = _.debounce (evaluator) -> 136 | evaluator.dumpProfilingInformation() 137 | ,5000 138 | 139 | makeYieldFilter = (aether) -> (engine, evaluator, e) -> 140 | 141 | frame_stack = evaluator.frames 142 | #console.log x.type + " " + x.ast?.type for x in frame_stack 143 | #console.log "----" 144 | 145 | top = frame_stack[0] 146 | 147 | 148 | if e? and e.type is 'event' and e.event is 'loopBodyStart' 149 | if top.srcAst.type is 'WhileStatement' and aether.options.alwaysYieldAtTopOfLoops 150 | if top.mark? then return true else top.mark = 1 151 | 152 | if top.srcAst.type is 'WhileStatement' and top.srcAst.test.type is 'Literal' 153 | if aether.whileLoopMarker? 154 | currentMark = aether.whileLoopMarker(top) 155 | if currentMark is top.mark 156 | #console.log "[Aether] Forcing while-true loop to yield, repeat #{currentMark}" 157 | top.mark = currentMark + 1 158 | return true 159 | else 160 | #console.log "[Aether] Loop Avoided, mark #{top.mark} isnt #{currentMark}" 161 | top.mark = currentMark 162 | 163 | if aether._shouldYield 164 | yieldValue = aether._shouldYield 165 | aether._shouldYield = false 166 | frame_stack[1].didYield = true if frame_stack[1].type is 'loop' 167 | return true 168 | 169 | return false 170 | 171 | module.exports.createThread = (aether, fx) -> 172 | internalFx = esper.Value.getBookmark fx 173 | engine = aether.esperEngine.fork() 174 | upgradeEvaluator aether, engine.evaluator 175 | return engine.makeFunctionFromClosure internalFx, makeYieldFilter(aether) 176 | 177 | module.exports.upgradeEvaluator = upgradeEvaluator = (aether, evaluator) -> 178 | executionCount = 0 179 | evaluator.instrument = (evalu, evt) -> 180 | debugDumper evaluator 181 | if ++executionCount > aether.options.executionLimit 182 | throw new TypeError 'Statement execution limit reached' 183 | updateState aether, evalu, evt 184 | 185 | 186 | emptyAST = {"type":"Program","body":[{"type":"FunctionDeclaration","id":{"type":"Identifier","name":"plan","range":[9,13],"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":13}},"originalRange":{"start":{"ofs":-8,"row":0,"col":-8},"end":{"ofs":-4,"row":0,"col":-4}}},"params":[],"defaults":[],"body":{"type":"BlockStatement","body":[{"type":"VariableDeclaration","declarations":[{"type":"VariableDeclarator","id":{"type":"Identifier","name":"hero"},"init":{"type":"ThisExpression"}}],"kind":"var","userCode":false}],"range":[16,19],"loc":{"start":{"line":1,"column":16},"end":{"line":2,"column":1}},"originalRange":{"start":{"ofs":-1,"row":0,"col":-1},"end":{"ofs":2,"row":1,"col":1}}},"rest":null,"generator":false,"expression":false,"range":[0,19],"loc":{"start":{"line":1,"column":0},"end":{"line":2,"column":1}},"originalRange":{"start":{"ofs":-17,"row":0,"col":-17},"end":{"ofs":2,"row":1,"col":1}}}],"range":[0,19],"loc":{"start":{"line":1,"column":0},"end":{"line":2,"column":1}},"originalRange":{"start":{"ofs":-17,"row":0,"col":-17},"end":{"ofs":2,"row":1,"col":1}}} 187 | -------------------------------------------------------------------------------- /src/languages/coffeescript.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | parserHolder = {} 4 | estraverse = require 'estraverse' 5 | 6 | Language = require './language' 7 | 8 | module.exports = class CoffeeScript extends Language 9 | name: 'CoffeeScript' 10 | id: 'coffeescript' 11 | parserID: 'csredux' 12 | thisValue:'@' 13 | thisValueAccess:'@' 14 | heroValueAccess:'hero.' 15 | wrappedCodeIndentLen: 4 16 | 17 | constructor: -> 18 | super arguments... 19 | @indent = Array(@wrappedCodeIndentLen + 1).join ' ' 20 | parserHolder.csredux ?= self?.aetherCoffeeScriptRedux ? require 'coffee-script-redux' 21 | 22 | # Wrap the user code in a function. Store @wrappedCodePrefix and @wrappedCodeSuffix. 23 | wrap: (rawCode, aether) -> 24 | @wrappedCodePrefix ?=""" 25 | #{aether.options.functionName or 'foo'} = (#{aether.options.functionParameters.join(', ')}) -> 26 | \n""" 27 | @wrappedCodeSuffix ?= '\n' 28 | indentedCode = (@indent + line for line in rawCode.split '\n').join '\n' 29 | @wrappedCodePrefix + indentedCode + @wrappedCodeSuffix 30 | 31 | removeWrappedIndent: (range) -> 32 | # Assumes range not in @wrappedCodePrefix 33 | range = _.cloneDeep range 34 | range[0].ofs -= @wrappedCodeIndentLen * range[0].row 35 | range[1].ofs -= @wrappedCodeIndentLen * range[1].row 36 | range 37 | 38 | # Using a third-party parser, produce an AST in the standardized Mozilla format. 39 | parse: (code, aether) -> 40 | csAST = parserHolder.csredux.parse code, {optimise: false, raw: true} 41 | jsAST = parserHolder.csredux.compile csAST, {bare: true} 42 | fixLocations jsAST 43 | jsAST 44 | 45 | 46 | class StructuredCode 47 | # TODO: What does this class do? 48 | constructor: (code) -> 49 | [@cursors, @indentations] = @generateOffsets code 50 | @length = @cursors.length 51 | 52 | generateOffsets: (code) -> 53 | reg = /(?:\r\n|[\r\n\u2028\u2029])/g 54 | result = [ 0 ] 55 | indentations = [ 0 ] 56 | while res = reg.exec(code) 57 | cursor = res.index + res[0].length 58 | reg.lastIndex = cursor 59 | result.push cursor 60 | indentations.push code.substr(cursor).match(/^\s+/)?[0]?.length 61 | [result, indentations] 62 | 63 | column: (offset) -> 64 | @loc(offset).column 65 | 66 | line: (offset) -> 67 | @loc(offset).line 68 | 69 | fixRange: (range, loc) -> 70 | fix = Math.floor(@indentations[loc.start.line-1]+5/4) 71 | range[0] -= fix 72 | range[1] -= fix 73 | range 74 | 75 | loc: (offset) -> 76 | index = _.sortedIndex @cursors, offset 77 | if @cursors.length > index and @cursors[index] is offset 78 | column = 0 79 | line = index + 1 80 | else 81 | column = offset - 4 - @cursors[index - 1] 82 | line = index 83 | { column, line } 84 | 85 | fixLocations = (program) -> 86 | # TODO: What does this function do? 87 | structured = new StructuredCode(program.raw) 88 | estraverse.traverse program, 89 | leave: (node, parent) -> 90 | if node.range? 91 | # calculate start line & column 92 | loc = 93 | start: null 94 | end: structured.loc(node.range[1]) 95 | if node.loc? 96 | loc.start = node.loc.start 97 | else 98 | loc.start = structured.loc(node.range[0]) 99 | if _.isNaN loc.end.column 100 | loc.end.column = loc.start.column + 1 # Fix for bad CSR(?) parsing of "Sammy the Python moved #{meters}m." 101 | node.loc = loc 102 | unless node.range[1]? 103 | node.range[1] = node.range[0] + 1 # Same #{meters} fix 104 | node.range = structured.fixRange(node.range, loc) 105 | else 106 | node.loc = switch node.type 107 | when 'BlockStatement' 108 | if node.body.length 109 | start: node.body[0].loc.start 110 | end: node.body[node.body.length - 1].loc.end 111 | else 112 | parent.loc 113 | when 'VariableDeclarator' 114 | if node?.init?.loc? 115 | start: node.id.loc.start 116 | end: node.init.loc.end 117 | else 118 | node.id.loc 119 | when 'ExpressionStatement' 120 | node.expression.loc 121 | when 'ReturnStatement' 122 | if node.argument? then node.argument.loc else node.loc 123 | when 'VariableDeclaration' 124 | start: node.declarations[0].loc.start 125 | end: node.declarations[node.declarations.length - 1].loc.end 126 | else 127 | start: {line: 0, column: 0} 128 | end: {line: 0, column: 0} 129 | return 130 | -------------------------------------------------------------------------------- /src/languages/html.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | Language = require './language' 4 | 5 | module.exports = class HTML extends Language 6 | name: 'HTML' 7 | id: 'html' 8 | parserID: 'html' 9 | 10 | constructor: -> 11 | super arguments... 12 | 13 | hasChangedASTs: (a, b) -> 14 | return a.replace(/\s/g) isnt b.replace(/\s/g) 15 | 16 | usesFunctionWrapping: -> false 17 | 18 | # TODO: think about what this stub should do, really. 19 | parse: (code, aether) -> 20 | return code 21 | 22 | replaceLoops: (rawCode) -> 23 | [rawCode, []] 24 | -------------------------------------------------------------------------------- /src/languages/java.coffee: -------------------------------------------------------------------------------- 1 | Language = require './language' 2 | parserHolder = {} 3 | 4 | module.exports = class Java extends Language 5 | name: 'Java' 6 | id: 'java' 7 | parserID: 'cashew' 8 | 9 | constructor: -> 10 | super arguments... 11 | parserHolder.cashew ?= self?.aetherCashew ? require 'cashew-js' 12 | @runtimeGlobals = ___JavaRuntime: parserHolder.cashew.___JavaRuntime, _Object: parserHolder.cashew._Object, Integer: parserHolder.cashew.Integer, Double: parserHolder.cashew.Double, _NotInitialized: parserHolder.cashew._NotInitialized, _ArrayList: parserHolder.cashew._ArrayList 13 | 14 | 15 | obviouslyCannotTranspile: (rawCode) -> 16 | false 17 | 18 | 19 | 20 | parse: (code, aether) -> 21 | ast = parserHolder.cashew.Parse code 22 | ast = parserHolder.cashew.wrapFunction ast, aether.options.functionName, aether.className, aether.staticCall 23 | ast 24 | -------------------------------------------------------------------------------- /src/languages/javascript.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | jshintHolder = {} 4 | esprima = require 'esprima' # getting our Esprima Harmony 5 | acorn_loose = require 'acorn/acorn_loose' # for if Esprima dies. Note it can't do ES6. 6 | escodegen = require 'escodegen' 7 | 8 | Language = require './language' 9 | traversal = require '../traversal' 10 | 11 | module.exports = class JavaScript extends Language 12 | name: 'JavaScript' 13 | id: 'javascript' 14 | parserID: 'esprima' 15 | thisValue: 'this' 16 | thisValueAccess: 'this.' 17 | heroValueAccess: 'hero.' 18 | 19 | constructor: -> 20 | super arguments... 21 | jshintHolder.jshint ?= (self?.aetherJSHint ? require('jshint')).JSHINT 22 | 23 | # Return true if we can very quickly identify a syntax error. 24 | obviouslyCannotTranspile: (rawCode) -> 25 | try 26 | # Inspired by ACE: https://github.com/ajaxorg/ace/blob/master/lib/ace/mode/javascript_worker.js 27 | eval "'use strict;'\nthrow 0;" + rawCode # evaluated code can only create variables in this function 28 | catch e 29 | return true unless e is 0 30 | false 31 | 32 | # Return true if there are significant (non-whitespace) differences in the ASTs for a and b. 33 | hasChangedASTs: (a, b) -> 34 | # We try first with Esprima, to be precise, then with acorn_loose if that doesn't work. 35 | options = {loc: false, range: false, comment: false, tolerant: true} 36 | [aAST, bAST] = [null, null] 37 | try aAST = esprima.parse a, options 38 | try bAST = esprima.parse b, options 39 | return true if (not aAST or not bAST) and (aAST or bAST) 40 | if aAST and bAST 41 | return true if (aAST.errors ? []).length isnt (bAST.errors ? []).length 42 | return not _.isEqual(aAST.body, bAST.body) 43 | # Esprima couldn't parse either ASTs, so let's fall back to acorn_loose 44 | options = {locations: false, tabSize: 4, ecmaVersion: 5} 45 | aAST = acorn_loose.parse_dammit a, options 46 | bAST = acorn_loose.parse_dammit b, options 47 | unless aAST and bAST 48 | console.log "Couldn't even loosely parse; are you sure #{a} and #{b} are #{@name}?" 49 | return true 50 | # acorn_loose annoyingly puts start/end in every node; we'll remove before comparing 51 | removeLocations = (node) -> node.start = node.end = null if node 52 | traversal.walkAST aAST, removeLocations 53 | traversal.walkAST bAST, removeLocations 54 | return not _.isEqual(aAST, bAST) 55 | 56 | 57 | # Return an array of problems detected during linting. 58 | lint: (rawCode, aether) -> 59 | lintProblems = [] 60 | return lintProblems unless jshintHolder.jshint 61 | wrappedCode = @wrap rawCode, aether 62 | 63 | # Run it through JSHint first, because that doesn't rely on Esprima 64 | # See also how ACE does it: https://github.com/ajaxorg/ace/blob/master/lib/ace/mode/javascript_worker.js 65 | # TODO: make JSHint stop providing these globals somehow; the below doesn't work 66 | jshintOptions = browser: false, couch: false, devel: false, dojo: false, jquery: false, mootools: false, node: false, nonstandard: false, phantom: false, prototypejs: false, rhino: false, worker: false, wsh: false, yui: false 67 | jshintGlobals = _.zipObject jshintGlobals, (false for g in aether.allGlobals) # JSHint expects {key: writable} globals 68 | # Doesn't work; can't find a way to skip warnings from JSHint programmatic options instead of in code comments. 69 | #for problemID, problem of @originalOptions.problems when problem.level is 'ignore' and /jshint/.test problemID 70 | # console.log 'gotta ignore', problem, '-' + problemID.replace('jshint_', '') 71 | # jshintOptions['-' + problemID.replace('jshint_', '')] = true 72 | try 73 | jshintSuccess = jshintHolder.jshint(wrappedCode, jshintOptions, jshintGlobals) 74 | catch e 75 | console.warn "JSHint died with error", e #, "on code\n", wrappedCode 76 | for error in jshintHolder.jshint.errors 77 | lintProblems.push aether.createUserCodeProblem type: 'transpile', reporter: 'jshint', error: error, code: wrappedCode, codePrefix: @wrappedCodePrefix 78 | 79 | # Check for stray semi-colon on 1st line of if statement 80 | # E.g. "if (parsely);" 81 | # TODO: Does not handle stray semi-colons on following lines: "if (parsely)\n;" 82 | if _.isEmpty lintProblems 83 | lines = rawCode.split /\r\n|[\n\r\u2028\u2029]/g 84 | offset = 0 85 | for line, row in lines 86 | if /^\s*if /.test(line) 87 | # Have an if statement 88 | if (firstParen = line.indexOf('(')) >= 0 89 | parenCount = 1 90 | for c, i in line[firstParen + 1..line.length] 91 | parenCount++ if c is '(' 92 | parenCount-- if c is ')' 93 | break if parenCount is 0 94 | # parenCount should be zero at the end of the if (test) 95 | i += firstParen + 1 + 1 96 | if parenCount is 0 and /^[ \t]*;/.test(line[i..line.length]) 97 | # And it's followed immediately by a semi-colon 98 | firstSemiColon = line.indexOf(';') 99 | lintProblems.push 100 | type: 'transpile' 101 | reporter: 'aether' 102 | level: 'warning' 103 | message: "Don't put a ';' after an if statement." 104 | range: [ 105 | ofs: offset + firstSemiColon 106 | row: row 107 | col: firstSemiColon 108 | , 109 | ofs: offset + firstSemiColon + 1 110 | row: row 111 | col: firstSemiColon + 1 112 | ] 113 | break 114 | # TODO: this may be off by 1*row if linebreak was \r\n 115 | offset += line.length + 1 116 | lintProblems 117 | 118 | # Return a beautified representation of the code (cleaning up indentation, etc.) 119 | beautify: (rawCode, aether) -> 120 | try 121 | ast = esprima.parse rawCode, {range: true, tokens: true, comment: true, tolerant: true} 122 | ast = escodegen.attachComments ast, ast.comments, ast.tokens 123 | catch e 124 | console.log 'got error beautifying', e 125 | ast = acorn_loose.parse_dammit rawCode, {tabSize: 4, ecmaVersion: 5} 126 | beautified = escodegen.generate ast, {comment: true, parse: esprima.parse} 127 | beautified 128 | 129 | usesFunctionWrapping: () -> false 130 | 131 | # Hacky McHack step for things we can't easily change via AST transforms (which preserve statement ranges). 132 | # TODO: Should probably refactor and get rid of this soon. 133 | hackCommonMistakes: (code, aether) -> 134 | # Stop this.\n from failing on the next weird line 135 | code = code.replace /this\.\s*?\n/g, "this.IncompleteThisReference;" 136 | # If we wanted to do it just when it would hit the ending } but allow multiline this refs: 137 | #code = code.replace /this.(\s+})$/, "this.IncompleteThisReference;$1" 138 | code 139 | 140 | # Using a third-party parser, produce an AST in the standardized Mozilla format. 141 | parse: (code, aether) -> 142 | # loc: https://github.com/codecombat/aether/issues/71 143 | ast = esprima.parse code, {range: true, loc: true, tolerant: true} 144 | errors = [] 145 | if ast.errors 146 | errors = (x for x in ast.errors when x.description isnt 'Illegal return statement') 147 | delete ast.errors 148 | 149 | throw errors[0] if errors[0] 150 | ast 151 | 152 | # Optional: if parseDammit() is implemented, then if parse() throws an error, we'll try again using parseDammit(). 153 | # Useful for parsing incomplete code as it is being written without giving up. 154 | # This should never throw an error and should always return some sort of AST, even if incomplete or empty. 155 | parseDammit: (code, aether) -> 156 | ast = acorn_loose.parse_dammit code, {locations: true, tabSize: 4, ecmaVersion: 5} 157 | 158 | if ast? and ast.body.length isnt 1 159 | ast.body = ast.body.slice(0,0) 160 | ast 161 | 162 | # Esprima uses "range", but acorn_loose only has "locations" 163 | lines = code.replace(/\n/g, '\n空').split '空' # split while preserving newlines 164 | posToOffset = (pos) -> 165 | _.reduce(lines.slice(0, pos.line - 1), ((sum, line) -> sum + line.length), 0) + pos.column 166 | # lines are 1-indexed, and I think columns are 0-indexed, but should verify 167 | locToRange = (loc) -> 168 | [posToOffset(loc.start), posToOffset(loc.end)] 169 | fixNodeRange = (node) -> 170 | # Sometimes you can get an if-statement with "alternate": null 171 | node.range = locToRange node.loc if node and node.loc 172 | traversal.walkAST ast, fixNodeRange 173 | 174 | ast 175 | -------------------------------------------------------------------------------- /src/languages/language.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | module.exports = class Language 4 | name: 'Abstract Language' # Display name of the programming language 5 | id: 'abstract-language' # Snake-case id of the programming language 6 | parserID: 'abstract-parser' 7 | runtimeGlobals: {} # Like {__lua: require('lua2js').runtime} 8 | thisValue: 'this' # E.g. in Python it is 'self' 9 | thisValueAccess: 'this.' # E.g. in Python it is 'self.' 10 | heroValueAccess: 'hero.' 11 | wrappedCodeIndentLen: 0 12 | 13 | constructor: -> 14 | 15 | # Return true if we can very quickly identify a syntax error. 16 | obviouslyCannotTranspile: (rawCode) -> 17 | false 18 | 19 | # Return true if there are significant (non-whitespace) differences in the ASTs for a and b. 20 | hasChangedASTs: (a, b) -> 21 | true 22 | 23 | # Return true if a and b have the same number of lines after we strip trailing comments and whitespace. 24 | hasChangedLineNumbers: (a, b) -> 25 | # This implementation will work for languages with comments starting with // 26 | # TODO: handle /* */ 27 | unless String.prototype.trimRight 28 | String.prototype.trimRight = -> String(@).replace /\s\s*$/, '' 29 | a = a.replace(/^[ \t]+\/\/.*/g, '').trimRight() 30 | b = b.replace(/^[ \t]+\/\/.*/g, '').trimRight() 31 | return a.split('\n').length isnt b.split('\n').length 32 | 33 | # Return an array of UserCodeProblems detected during linting. 34 | lint: (rawCode, aether) -> 35 | [] 36 | 37 | # Return a beautified representation of the code (cleaning up indentation, etc.) 38 | beautify: (rawCode, aether) -> 39 | rawCode 40 | 41 | # Wrap the user code in a function. Store @wrappedCodePrefix and @wrappedCodeSuffix. 42 | wrap: (rawCode, aether) -> 43 | @wrappedCodePrefix ?= '' 44 | @wrappedCodeSuffix ?= '' 45 | @wrappedCodePrefix + rawCode + @wrappedCodeSuffix 46 | 47 | # Languages requiring extra indent in their wrapped code may need to remove it from ranges 48 | # E.g. Python 49 | removeWrappedIndent: (range) -> 50 | range 51 | 52 | # Hacky McHack step for things we can't easily change via AST transforms (which preserve statement ranges). 53 | # TODO: Should probably refactor and get rid of this soon. 54 | hackCommonMistakes: (rawCode, aether) -> 55 | rawCode 56 | 57 | # Using a third-party parser, produce an AST in the standardized Mozilla format. 58 | parse: (code, aether) -> 59 | throw new Error "parse() not implemented for #{@id}." 60 | 61 | # Optional: if parseDammit() is implemented, then if parse() throws an error, we'll try again using parseDammit(). 62 | # Useful for parsing incomplete code as it is being written without giving up. 63 | # This should never throw an error and should always return some sort of AST, even if incomplete or empty. 64 | #parseDammit: (code, aether) -> 65 | 66 | # Convert obj to a language-specific type 67 | # E.g. if obj is an Array and language is Python, return a Python list 68 | convertToNativeType: (obj) -> 69 | obj 70 | 71 | usesFunctionWrapping: () -> 72 | true 73 | 74 | cloneObj: (obj, cloneFn=(o) -> o) -> 75 | # Clone obj to a language-specific equivalent object 76 | # E.g. if obj is an Array and language is Python, we want a new Python list instead of a JavaScript Array. 77 | # Use cloneFn for children and simple types 78 | if _.isArray obj 79 | result = (cloneFn v for v in obj) 80 | else if _.isObject obj 81 | result = {} 82 | result[k] = cloneFn v for k, v of obj 83 | else 84 | result = cloneFn obj 85 | result 86 | 87 | pryOpenCall: (call, val, finder) -> 88 | null 89 | 90 | rewriteFunctionID: (fid) -> 91 | fid 92 | 93 | setupInterpreter: (esper) -> 94 | -------------------------------------------------------------------------------- /src/languages/languages.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | javascript: require './javascript' 3 | coffeescript: require './coffeescript' 4 | python: require './python' 5 | lua: require './lua' 6 | java: require './java' 7 | html: require './html' 8 | -------------------------------------------------------------------------------- /src/languages/lua.coffee: -------------------------------------------------------------------------------- 1 | Language = require './language' 2 | ranges = require '../ranges' 3 | parserHolder = {} 4 | 5 | module.exports = class Lua extends Language 6 | name: 'Lua' 7 | id: 'lua' 8 | parserID: 'lua2js' 9 | heroValueAccess: 'hero:' 10 | 11 | constructor: -> 12 | super arguments... 13 | parserHolder.lua2js ?= self?.aetherLua2JS ? require 'lua2js' 14 | @runtimeGlobals = parserHolder.lua2js.stdlib 15 | # TODO: remove injectCode when we set language in Esper to let Esper use native language code 16 | @injectCode = require 'aether-lang-stdlibs/lua-stdlib.ast.json' 17 | @fidMap = {} 18 | 19 | obviouslyCannotTranspile: (rawCode) -> 20 | false 21 | 22 | callParser: (code, loose) -> 23 | ast = parserHolder.lua2js.parse code, {loose: loose, forceVar: false, decorateLuaObjects: true, luaCalls: true, luaOperators: true, encloseWithFunctions: false } 24 | ast 25 | 26 | 27 | # Return an array of problems detected during linting. 28 | lint: (rawCode, aether) -> 29 | lintProblems = [] 30 | 31 | try 32 | ast = @callParser rawCode, true 33 | catch e 34 | return [] 35 | return [aether.createUserCodeProblem type: 'transpile', reporter: 'lua2js', error: e, code:rawCode, codePrefix: ""] 36 | for error in ast.errors 37 | rng = ranges.offsetsToRange(error.range[0], error.range[1], rawCode, '') 38 | lintProblems.push aether.createUserCodeProblem type: 'transpile', reporter: 'lua2js', message: error.msg, code: rawCode, codePrefix: "", range: [rng.start, rng.end] 39 | 40 | lintProblems 41 | 42 | usesFunctionWrapping: () -> false 43 | 44 | wrapResult: (ast, name, params) -> 45 | ast.body.unshift {"type": "VariableDeclaration","declarations": [ 46 | { "type": "VariableDeclarator", "id": {"type": "Identifier", "name": "self" },"init": {"type": "ThisExpression"} } 47 | ],"kind": "var", "userCode": false} 48 | ast 49 | 50 | parse: (code, aether) -> 51 | ast = Lua.prototype.wrapResult (Lua.prototype.callParser code, false), aether.options.functionName, aether.options.functionParameters 52 | ast 53 | 54 | 55 | parseDammit: (code, aether) -> 56 | try 57 | ast = Lua.prototype.wrapResult (Lua.prototype.callParser code, true), aether.options.functionName, aether.options.functionParameters 58 | return ast 59 | catch error 60 | return {"type": "BlockStatement": body:[{type: "EmptyStatement"}]} 61 | 62 | pryOpenCall: (call, val, finder) -> 63 | node = call.right 64 | if val[1] != "__lua" 65 | return null 66 | 67 | if val[2] == "call" 68 | target = node.arguments[1] 69 | #if @fidMap[target.name] 70 | # return @fidMap[target] 71 | return finder(target) 72 | 73 | if val[2] == "makeFunction" 74 | @fidMap[node.arguments[0].name] = finder(call.left) 75 | 76 | return null 77 | 78 | rewriteFunctionID: (fid) -> 79 | @fidMap[fid] or fid 80 | -------------------------------------------------------------------------------- /src/languages/python.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | parserHolder = {} 4 | traversal = require '../traversal' 5 | Language = require './language' 6 | 7 | module.exports = class Python extends Language 8 | name: 'Python' 9 | id: 'python' 10 | parserID: 'filbert' 11 | thisValue: 'self' 12 | thisValueAccess: 'self.' 13 | heroValueAccess: 'hero.' 14 | wrappedCodeIndentLen: 4 15 | 16 | constructor: -> 17 | super arguments... 18 | # TODO: remove injectCode when we set language in Esper to let Esper use native language code 19 | @injectCode = require 'aether-lang-stdlibs/python-stdlib.ast.json' 20 | @indent = Array(@wrappedCodeIndentLen + 1).join ' ' 21 | unless parserHolder.parser?.pythonRuntime? 22 | if parserHolder.parser? 23 | console.log 'Aether python parser ONLY missing pythonRuntime' 24 | parserHolder.parser = self?.aetherFilbert ? require 'skulpty' 25 | unless parserHolder.parser.pythonRuntime 26 | console.error "Couldn't import Python runtime; our filbert import only gave us", parserHolder.parser 27 | parserHolder.parserLoose ?= self?.aetherFilbertLoose ? require 'skulpty' 28 | @runtimeGlobals = 29 | __pythonRuntime: parserHolder.parser.pythonRuntime 30 | 31 | hasChangedASTs: (a, b) -> 32 | try 33 | [aAST, bAST] = [null, null] 34 | options = {locations: false, ranges: false} 35 | aAST = parserHolder.parserLoose.parse_dammit a, options 36 | bAST = parserHolder.parserLoose.parse_dammit b, options 37 | unless aAST and bAST 38 | return true 39 | return not _.isEqual(aAST, bAST) 40 | catch error 41 | return true 42 | 43 | 44 | # Return an array of UserCodeProblems detected during linting. 45 | lint: (rawCode, aether) -> 46 | problems = [] 47 | 48 | try 49 | ast = parserHolder.parser.parse rawCode, locations: true, ranges: true, allowReturnOutsideFunction: true 50 | 51 | # Check for empty loop 52 | traversal.walkASTCorrect ast, (node) => 53 | return unless node.type is "WhileStatement" 54 | return unless node.body.body.length is 0 55 | # Craft an warning for empty loop 56 | problems.push 57 | type: 'transpile' 58 | reporter: 'aether' 59 | level: 'warning' 60 | message: "Empty loop. Put 4 spaces in front of statements inside loops." 61 | range: [ 62 | ofs: node.range[0] 63 | row: node.loc.start.line - 1 64 | col: node.loc.start.column 65 | , 66 | ofs: node.range[1] 67 | row: node.loc.end.line - 1 68 | col: node.loc.end.column 69 | ] 70 | 71 | # Check for empty if 72 | if problems.length is 0 73 | traversal.walkASTCorrect ast, (node) => 74 | return unless node.type is "IfStatement" 75 | return unless node.consequent.body.length is 0 76 | # Craft an warning for empty loop 77 | problems.push 78 | type: 'transpile' 79 | reporter: 'aether' 80 | level: 'warning' 81 | # TODO: Try 'belong to' instead of 'inside' if players still have problems 82 | message: "Empty if statement. Put 4 spaces in front of statements inside the if statement." 83 | range: [ 84 | ofs: node.range[0] 85 | row: node.loc.start.line - 1 86 | col: node.loc.start.column 87 | , 88 | ofs: node.range[1] 89 | row: node.loc.end.line - 1 90 | col: node.loc.end.column 91 | ] 92 | 93 | catch error 94 | 95 | problems 96 | 97 | usesFunctionWrapping: () -> false 98 | 99 | removeWrappedIndent: (range) -> 100 | # Assumes range not in @wrappedCodePrefix 101 | range = _.cloneDeep range 102 | range 103 | 104 | # Using a third-party parser, produce an AST in the standardized Mozilla format. 105 | parse: (code, aether) -> 106 | ast = parserHolder.parser.parse code, {locations: false, ranges: true, allowReturnOutsideFunction: true} 107 | selfToThis ast 108 | ast 109 | 110 | parseDammit: (code, aether) -> 111 | try 112 | ast = parserHolder.parserLoose.parse_dammit code, {locations: false, ranges: true} 113 | selfToThis ast 114 | catch error 115 | ast = {type: "Program", body:[{"type": "EmptyStatement"}]} 116 | ast 117 | 118 | convertToNativeType: (obj) -> 119 | parserHolder.parser.pythonRuntime.utils.convertToList(obj) if not obj?._isPython and _.isArray obj 120 | parserHolder.parser.pythonRuntime.utils.convertToDict(obj) if not obj?._isPython and _.isObject obj 121 | obj 122 | 123 | cloneObj: (obj, cloneFn=(o) -> o) -> 124 | if _.isArray obj 125 | result = new parserHolder.parser.pythonRuntime.objects.list() 126 | result.append(cloneFn v) for v in obj 127 | else if _.isObject obj 128 | result = new parserHolder.parser.pythonRuntime.objects.dict() 129 | result[k] = cloneFn v for k, v of obj 130 | else 131 | result = cloneFn obj 132 | result 133 | 134 | selfToThis = (ast) -> 135 | ast.body.unshift {"type": "VariableDeclaration","declarations": [{ "type": "VariableDeclarator", "id": {"type": "Identifier", "name": "self" },"init": {"type": "ThisExpression"} }],"kind": "var", "userCode": false} # var self = this; 136 | ast 137 | 138 | setupInterpreter: (esper) -> 139 | realm = esper.realm 140 | realm.options.linkValueCallReturnValueWrapper = (value) -> 141 | ArrayPrototype = realm.ArrayPrototype 142 | 143 | return value unless value.jsTypeName is 'object' 144 | 145 | if value.clazz is 'Array' 146 | defineProperties = realm.Object.getImmediate('defineProperties'); 147 | listPropertyDescriptor = realm.globalScope.get('__pythonRuntime').getImmediate('utils').getImmediate('listPropertyDescriptor'); 148 | 149 | gen = defineProperties.call realm.Object, [value, listPropertyDescriptor], realm.globalScope 150 | it = gen.next() 151 | while not it.done 152 | it = gen.next() 153 | 154 | return value 155 | -------------------------------------------------------------------------------- /src/problems.coffee: -------------------------------------------------------------------------------- 1 | ranges = require './ranges' 2 | string_score = require 'string_score' 3 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 4 | 5 | # Problems ################################# 6 | # 7 | # Error messages and hints: 8 | # Processed by markdown 9 | # In general, put correct replacement code in a markdown code span. E.g. "Try `self.moveRight()`" 10 | # 11 | # 12 | # Problem Context (problemContext) 13 | # 14 | # Aether accepts a problemContext parameter via the constructor options or directly to createUserCodeProblem 15 | # This context can be used to craft better errors messages. 16 | # 17 | # Example: 18 | # Incorrect user code is 'this.attack(Brak);' 19 | # Correct user code is 'this.attack("Brak");' 20 | # Error: 'Brak is undefined' 21 | # If we had a list of expected string references, we could provide a better error message: 22 | # 'Brak is undefined. Are you missing quotes? Try this.attack("Brak");' 23 | # 24 | # Available Context Properties: 25 | # stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak) 26 | # thisMethods: methods available on the 'this' object 27 | # thisProperties: properties available on the 'this' object 28 | # commonThisMethods: methods that are available sometimes, but not awlays 29 | # 30 | 31 | # Esprima Harmony's error messages track V8's 32 | # https://github.com/ariya/esprima/blob/harmony/esprima.js#L194 33 | 34 | # JSHint's error and warning messages 35 | # https://github.com/jshint/jshint/blob/master/src/messages.js 36 | 37 | scoreFuzziness = 0.8 38 | acceptMatchThreshold = 0.5 39 | 40 | module.exports.createUserCodeProblem = (options) -> 41 | options ?= {} 42 | options.aether ?= @ # Can either be called standalone or as an Aether method 43 | if options.type is 'transpile' and options.error 44 | extractTranspileErrorDetails options 45 | if options.type is 'runtime' 46 | extractRuntimeErrorDetails options 47 | 48 | reporter = options.reporter or 'unknown' # Source of the problem, like 'jshint' or 'esprima' or 'aether' 49 | kind = options.kind or 'Unknown' # Like 'W075' or 'InvalidLHSInAssignment' 50 | id = reporter + '_' + kind # Uniquely identifies reporter + kind combination 51 | config = options.aether?.options?.problems?[id] or {} # Default problem level/message/hint overrides 52 | 53 | p = isUserCodeProblem: true 54 | p.id = id 55 | p.level = config.level or options.level or 'error' # 'error', 'warning', 'info' 56 | p.type = options.type or 'generic' # Like 'runtime' or 'transpile', maybe later 'lint' 57 | p.message = config.message or options.message or "Unknown #{p.type} #{p.level}" # Main error message (short phrase) 58 | p.hint = config.hint or options.hint or '' # Additional details about error message (sentence) 59 | p.range = options.range # Like [{ofs: 305, row: 15, col: 15}, {ofs: 312, row: 15, col: 22}], or null 60 | p.userInfo = options.userInfo ? {} # Record extra information with the error here 61 | p 62 | 63 | 64 | # Transpile Errors 65 | 66 | extractTranspileErrorDetails = (options) -> 67 | code = options.code or '' 68 | codePrefix = options.codePrefix or '' 69 | error = options.error 70 | options.message = error.message 71 | errorContext = options.problemContext or options.aether?.options?.problemContext 72 | languageID = options.aether?.options?.language 73 | 74 | originalLines = code.slice(codePrefix.length).split '\n' 75 | lineOffset = codePrefix.split('\n').length - 1 76 | 77 | # TODO: move these into language-specific plugins 78 | switch options.reporter 79 | when 'jshint' 80 | options.message ?= error.reason 81 | options.kind ?= error.code 82 | 83 | # TODO: Put this transpile error hint creation somewhere reasonable 84 | if doubleVar = options.message.match /'([\w]+)' is already defined\./ 85 | # TODO: Check that it's a var and not a function 86 | options.hint = "Don't use the 'var' keyword for '#{doubleVar[1]}' the second time." 87 | 88 | unless options.level 89 | options.level = {E: 'error', W: 'warning', I: 'info'}[error.code[0]] 90 | line = error.line - codePrefix.split('\n').length 91 | if line >= 0 92 | if error.evidence?.length 93 | startCol = originalLines[line].indexOf error.evidence 94 | endCol = startCol + error.evidence.length 95 | else 96 | [startCol, endCol] = [0, originalLines[line].length - 1] 97 | # TODO: no way this works; what am I doing with code prefixes? 98 | options.range = [ranges.rowColToPos(line, startCol, code, codePrefix), 99 | ranges.rowColToPos(line, endCol, code, codePrefix)] 100 | else 101 | # TODO: if we type an unmatched {, for example, then it thinks that line -2's function wrapped() { is unmatched... 102 | # TODO: no way this works; what am I doing with code prefixes? 103 | options.range = [ranges.offsetToPos(0, code, codePrefix), 104 | ranges.offsetToPos(code.length - 1, code, codePrefix)] 105 | when 'esprima' 106 | # TODO: column range should extend to whole token. Mod Esprima, or extend to end of line? 107 | # TODO: no way this works; what am I doing with code prefixes? 108 | options.range = [ranges.rowColToPos(error.lineNumber - 1 - lineOffset, error.column - 1, code, codePrefix), 109 | ranges.rowColToPos(error.lineNumber - 1 - lineOffset, error.column, code, codePrefix)] 110 | when 'acorn_loose' 111 | null 112 | when 'csredux' 113 | options.range = [ranges.rowColToPos(error.lineNumber - 1 - lineOffset, error.column - 1, code, codePrefix), 114 | ranges.rowColToPos(error.lineNumber - 1 - lineOffset, error.column, code, codePrefix)] 115 | when 'aether' 116 | null 117 | when 'closer' 118 | if error.startOffset and error.endOffset 119 | range = ranges.offsetsToRange(error.startOffset, error.endOffset, code) 120 | options.range = [range.start, range.end] 121 | when 'lua2js' 122 | options.message ?= error.message 123 | rng = ranges.offsetsToRange(error.offset, error.offset, code, '') 124 | options.range = [rng.start, rng.end] 125 | when 'filbert' 126 | if error.loc 127 | columnOffset = 0 128 | # filbert lines are 1-based, columns are 0-based 129 | row = error.loc.line - lineOffset - 1 130 | col = error.loc.column - columnOffset 131 | start = ranges.rowColToPos(row, col, code, codePrefix) 132 | end = ranges.rowColToPos(row, col + error.raisedAt - error.pos, code, codePrefix) 133 | options.range = [start, end] 134 | when 'iota' 135 | null 136 | when 'cashew' 137 | options.range = [ranges.offsetToPos(error.range[0], code, codePrefix), 138 | ranges.offsetToPos(error.range[1], code, codePrefix)] 139 | options.hint = error.message 140 | else 141 | console.warn "Unhandled UserCodeProblem reporter", options.reporter 142 | 143 | options.hint = error.hint or getTranspileHint options.message, errorContext, languageID, options.aether.raw, options.range, options.aether.options?.simpleLoops 144 | options 145 | 146 | getTranspileHint = (msg, context, languageID, code, range, simpleLoops=false) -> 147 | #console.log 'get transpile hint', msg, context, languageID, code, range 148 | # TODO: Only used by Python currently 149 | # TODO: JavaScript blocked by jshint range bug: https://github.com/codecombat/aether/issues/113 150 | if msg in ["Unterminated string constant", "Unclosed string."] and range? 151 | codeSnippet = code.substring range[0].ofs, range[1].ofs 152 | # Trim codeSnippet so we can construct the correct suggestion with an ending quote 153 | firstQuoteIndex = codeSnippet.search /['"]/ 154 | if firstQuoteIndex isnt -1 155 | quoteCharacter = codeSnippet[firstQuoteIndex] 156 | codeSnippet = codeSnippet.slice firstQuoteIndex + 1 157 | codeSnippet = codeSnippet.substring 0, nonAlphNumMatch.index if nonAlphNumMatch = codeSnippet.match /[^\w]/ 158 | return "Missing a quotation mark. Try `#{quoteCharacter}#{codeSnippet}#{quoteCharacter}`" 159 | 160 | else if msg is "Unexpected indent" 161 | if range? 162 | index = range[0].ofs 163 | index-- while index > 0 and /\s/.test(code[index]) 164 | if index >= 3 and /else/.test(code.substring(index - 3, index + 1)) 165 | return "You are missing a ':' after 'else'. Try `else:`" 166 | return "Code needs to line up." 167 | 168 | else if ((msg.indexOf("Unexpected token") >= 0) or (msg.indexOf("Unexpected identifier") >= 0)) and context? 169 | codeSnippet = code.substring range[0].ofs, range[1].ofs 170 | lineStart = code.substring range[0].ofs - range[0].col, range[0].ofs 171 | lineStartLow = lineStart.toLowerCase() 172 | # console.log "Aether transpile problem codeSnippet='#{codeSnippet}' lineStart='#{lineStart}'" 173 | 174 | # Check for extra thisValue + space at beginning of line 175 | # E.g. 'self self.moveRight()' 176 | hintCreator = new HintCreator context, languageID 177 | if lineStart.indexOf(hintCreator.thisValue) is 0 and lineStart.trim().length < lineStart.length 178 | # TODO: update error range so this extra bit is highlighted 179 | if codeSnippet.indexOf(hintCreator.thisValue) is 0 180 | return "Delete extra `#{hintCreator.thisValue}`" 181 | else 182 | return hintCreator.getReferenceErrorHint codeSnippet 183 | 184 | # Check for two commands on a single line with no semi-colon 185 | # E.g. "self.moveRight()self.moveDown()" 186 | # Check for problems following a ')' 187 | prevIndex = range[0].ofs - 1 188 | prevIndex-- while prevIndex >= 0 and /[\t ]/.test(code[prevIndex]) 189 | if prevIndex >= 0 and code[prevIndex] is ')' 190 | if codeSnippet is ')' 191 | return "Delete extra `)`" 192 | else if not /^\s*$/.test(codeSnippet) 193 | return "Put each command on a separate line" 194 | 195 | parens = 0 196 | parens += (if c is '(' then 1 else if c is ')' then -1 else 0) for c in lineStart 197 | return "Your parentheses must match." unless parens is 0 198 | 199 | # Check for uppercase loop 200 | # TODO: Should get 'loop' from problem context 201 | if simpleLoops and codeSnippet is ':' and lineStart isnt lineStartLow and lineStartLow is 'loop' 202 | return "Should be lowercase. Try `loop`" 203 | 204 | # Check for malformed if statements 205 | if /^\s*if /.test(lineStart) 206 | if codeSnippet is ':' 207 | return "Your if statement is missing a test clause. Try `if True:`" 208 | else if /^\s*$/.test(codeSnippet) 209 | # TODO: Upate error range to be around lineStart in this case 210 | return "You are missing a ':' after '#{lineStart}'. Try `#{lineStart}:`" 211 | 212 | # Catchall hint for 'Unexpected token' error 213 | if /Unexpected [token|identifier]/.test(msg) 214 | return "There is a problem with your code." 215 | 216 | # Runtime Errors 217 | 218 | extractRuntimeErrorDetails = (options) -> 219 | if error = options.error 220 | options.kind ?= error.name # I think this will pick up [Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, DOMException] 221 | 222 | if options.aether.options.useInterpreter 223 | options.message = error.toString() 224 | else 225 | options.message = error.message or error.toString() 226 | console.log("Extracting", error) 227 | options.hint = error.hint or getRuntimeHint options 228 | options.level ?= error.level 229 | options.userInfo ?= error.userInfo 230 | 231 | # NOTE: lastStatementRange set via instrumentation.logStatementStart(originalNode.originalRange) 232 | options.range ?= options.aether?.lastStatementRange 233 | 234 | 235 | if options.range? 236 | lineNumber = options.range[0].row + 1 237 | if options.message.search(/^Line \d+/) != -1 238 | options.message = options.message.replace /^Line \d+/, (match, n) -> "Line #{lineNumber}" 239 | else 240 | options.message = "Line #{lineNumber}: #{options.message}" 241 | 242 | getRuntimeHint = (options) -> 243 | code = options.aether.raw or '' 244 | context = options.problemContext or options.aether.options?.problemContext 245 | languageID = options.aether.options?.language 246 | simpleLoops = options.aether.options?.simpleLoops 247 | 248 | # Check stack overflow 249 | return "Did you call a function recursively?" if options.message is "RangeError: Maximum call stack size exceeded" 250 | 251 | # Check loop ReferenceError 252 | if simpleLoops and languageID is 'python' and /ReferenceError: loop is not defined/.test options.message 253 | # TODO: move this language-specific stuff to language-specific code 254 | if options.range? 255 | index = options.range[1].ofs 256 | index++ while index < code.length and /[^\n:]/.test code[index] 257 | hint = "You are missing a ':' after 'loop'. Try `loop:`" if index >= code.length or code[index] is '\n' 258 | else 259 | hint = "Are you missing a ':' after 'loop'? Try `loop:`" 260 | return hint 261 | 262 | # Use problemContext to add hints 263 | return unless context? 264 | hintCreator = new HintCreator context, languageID 265 | hintCreator.getHint code, options 266 | 267 | class HintCreator 268 | # Create hints for an error message based on a problem context 269 | # TODO: better class name, move this to a separate file 270 | 271 | constructor: (context, languageID) -> 272 | # TODO: move this language-specific stuff to language-specific code 273 | @thisValue = switch languageID 274 | when 'python' then 'self' 275 | when 'coffeescript' then '@' 276 | else 'this' 277 | @realThisValueAccess = switch languageID 278 | when 'python' then 'self.' 279 | when 'coffeescript' then '@' 280 | else 'this.' 281 | 282 | @thisValueAlias = context.thisValueAlias ? 'hero' 283 | # We use `hero` as `this` in CodeCombat now, so all `this` related hints 284 | # we get in the problem context should really reference `hero`. 285 | # Customizable via context, for example, `game` instead of `hero`. 286 | @thisValueAccess = switch languageID 287 | when 'python' then "#{@thisValueAlias}." 288 | when 'coffeescript' then "#{@thisValueAlias}." 289 | when 'lua' then "#{@thisValueAlias}:" 290 | else "#{@thisValueAlias}." 291 | 292 | @newVariableTemplate = switch languageID 293 | when 'javascript' then _.template('var <%= name %> = ') 294 | else _.template('<%= name %> = ') 295 | @methodRegex = switch languageID 296 | when 'python' then new RegExp "self\\.(\\w+)\\s*\\(" 297 | when 'coffeescript' then new RegExp "@(\\w+)\\s*\\(" 298 | else new RegExp "this\\.(\\w+)\\(" 299 | 300 | @context = context ? {} 301 | 302 | getHint: (code, {message, range, error, aether}) -> 303 | return unless @context? 304 | if error.code is 'UndefinedVariable' and error.when is 'write' and aether.language.id is 'javascript' 305 | return "Missing `var`. Use `var #{error.ident} =` to make a new variable." 306 | 307 | if error.code is "CallNonFunction" 308 | ast = error.targetAst 309 | if ast.type is "MemberExpression" and not ast.computed 310 | extra = "" 311 | target = ast.property.name 312 | if error.candidates? 313 | candidatesLow = (s.toLowerCase() for s in error.candidates) 314 | idx = candidatesLow.indexOf(target.toLowerCase()) 315 | if idx isnt -1 316 | newName = error.targetName.replace target, error.candidates[idx] 317 | return "Look out for capitalization: `#{error.targetName}` should be `#{newName}`." 318 | sm = @getScoreMatch target, [{candidates: error.candidates, msgFormatFn: (match) -> match}] 319 | if sm? 320 | newName = error.targetName.replace target, sm 321 | return "Look out for spelling issues: did you mean `#{newName}` instead of `#{error.targetName}`?" 322 | 323 | return "`#{ast.object.srcName}` has no method `#{ast.property.name}`." 324 | 325 | if (missingMethodMatch = message.match(/has no method '(.*?)'/)) or message.match(/is not a function/) or message.match(/has no method/) 326 | # NOTE: We only get this for valid thisValue and parens: self.blahblah() 327 | # NOTE: We get different error messages for this based on javascript engine: 328 | # Chrome: 'undefined is not a function' 329 | # Firefox: 'tmp5[tmp6] is not a function' 330 | # test framework: 'Line 1: Object # has no method 'moveright' 331 | if missingMethodMatch 332 | target = missingMethodMatch[1] 333 | else if range? 334 | # TODO: this is not covered by any test cases yet, because our test environment throws different errors 335 | codeSnippet = code.substring range[0].ofs, range[1].ofs 336 | missingMethodMatch = @methodRegex.exec codeSnippet 337 | target = missingMethodMatch[1] if missingMethodMatch? 338 | hint = if target? then @getNoFunctionHint target 339 | else if missingReference = message.match /([^\s]+) is not defined/ 340 | hint = @getReferenceErrorHint missingReference[1] 341 | else if missingProperty = message.match /Cannot (?:read|call) (?:property|method) '([\w]+)' of (?:undefined|null)/ 342 | # Chrome: "Cannot read property 'moveUp' of undefined" 343 | # TODO: Firefox: "tmp5 is undefined" 344 | hint = @getReferenceErrorHint missingProperty[1] 345 | 346 | # Chrome: "Cannot read property 'pos' of null" 347 | # TODO: Firefox: "tmp10 is null" 348 | # TODO: range is pretty busted, but row seems ok so we'll use that. 349 | # TODO: Should we use a different message if object was 'undefined' instead of 'null'? 350 | if not hint? and range? 351 | line = code.substring range[0].ofs - range[0].col, code.indexOf('\n', range[1].ofs) 352 | nullObjRegex = new RegExp "(\\w+)\\.#{missingProperty[1]}" 353 | if nullObjMatch = nullObjRegex.exec line 354 | hint = "'#{nullObjMatch[1]}' was null. Use a null check before accessing properties. Try `if #{nullObjMatch[1]}:`" 355 | hint 356 | 357 | getNoFunctionHint: (target) -> 358 | # Check thisMethods 359 | hint = @getNoCaseMatch target, @context.thisMethods, (match) => 360 | # TODO: Remove these format tests someday. 361 | # "Uppercase or lowercase problem. Try #{@thisValueAccess}#{match}()" 362 | # "Uppercase or lowercase problem. \n \n\tTry: #{@thisValueAccess}#{match}() \n\tHad: #{codeSnippet}" 363 | # "Uppercase or lowercase problem. \n \nTry: \n`#{@thisValueAccess}#{match}()` \n \nInstead of: \n`#{codeSnippet}`" 364 | "Uppercase or lowercase problem. Try `#{@thisValueAccess}#{match}()`" 365 | hint ?= @getScoreMatch target, [candidates: @context.thisMethods, msgFormatFn: (match) => 366 | "Try `#{@thisValueAccess}#{match}()`"] 367 | # Check commonThisMethods 368 | hint ?= @getExactMatch target, @context.commonThisMethods, (match) -> 369 | "You do not have an item equipped with the #{match} skill." 370 | hint ?= @getNoCaseMatch target, @context.commonThisMethods, (match) -> 371 | "Did you mean #{match}? You do not have an item equipped with that skill." 372 | hint ?= @getScoreMatch target, [candidates: @context.commonThisMethods, msgFormatFn: (match) -> 373 | "Did you mean #{match}? You do not have an item equipped with that skill."] 374 | hint ?= "You don't have a `#{target}` method." 375 | hint 376 | 377 | getReferenceErrorHint: (target) -> 378 | # Check missing quotes 379 | hint = @getExactMatch target, @context.stringReferences, (match) -> 380 | "Missing quotes. Try `\"#{match}\"`" 381 | # Check this props 382 | hint ?= @getExactMatch target, @context.thisMethods, (match) => 383 | "Try `#{@thisValueAccess}#{match}()`" 384 | hint ?= @getExactMatch target, @context.thisProperties, (match) => 385 | "Try `#{@thisValueAccess}#{match}`" 386 | # Check case-insensitive, quotes, this props 387 | if not hint? and target.toLowerCase() is @thisValue.toLowerCase() 388 | hint = "Uppercase or lowercase problem. Try `#{@thisValue}`" 389 | hint ?= @getNoCaseMatch target, @context.stringReferences, (match) -> 390 | "Missing quotes. Try `\"#{match}\"`" 391 | hint ?= @getNoCaseMatch target, @context.thisMethods, (match) => 392 | "Try `#{@thisValueAccess}#{match}()`" 393 | hint ?= @getNoCaseMatch target, @context.thisProperties, (match) => 394 | "Try `#{@thisValueAccess}#{match}`" 395 | # Check score match, quotes, this props 396 | hint ?= @getScoreMatch target, [ 397 | {candidates: [@thisValue], msgFormatFn: (match) -> "Try `#{match}`"}, 398 | {candidates: @context.stringReferences, msgFormatFn: (match) -> "Missing quotes. Try `\"#{match}\"`"}, 399 | {candidates: @context.thisMethods, msgFormatFn: (match) => "Try `#{@thisValueAccess}#{match}()`"}, 400 | {candidates: @context.thisProperties, msgFormatFn: (match) => "Try `#{@thisValueAccess}#{match}`"}] 401 | # Check commonThisMethods 402 | hint ?= @getExactMatch target, @context.commonThisMethods, (match) -> 403 | "You do not have an item equipped with the #{match} skill." 404 | hint ?= @getNoCaseMatch target, @context.commonThisMethods, (match) -> 405 | "Did you mean #{match}? You do not have an item equipped with that skill." 406 | hint ?= @getScoreMatch target, [candidates: @context.commonThisMethods, msgFormatFn: (match) -> 407 | "Did you mean #{match}? You do not have an item equipped with that skill."] 408 | # Check enemy defined 409 | if not hint and target.toLowerCase().indexOf('enemy') > -1 and _.contains(@context.thisMethods, 'findNearestEnemy') 410 | hint = "There is no `#{target}`. Use `#{@newVariableTemplate({name:target})}#{@thisValueAccess}findNearestEnemy()` first." 411 | 412 | # Try score match with this value prefixed 413 | # E.g. target = 'selfmoveright', try 'self.moveRight()'' 414 | if not hint? and @context?.thisMethods? 415 | thisPrefixed = (@thisValueAccess + method for method in @context.thisMethods) 416 | hint = @getScoreMatch target, [candidates: thisPrefixed, msgFormatFn: (match) -> 417 | "Try `#{match}()`"] 418 | hint 419 | 420 | getExactMatch: (target, candidates, msgFormatFn) -> 421 | return unless candidates? 422 | msgFormatFn target if target in candidates 423 | 424 | getNoCaseMatch: (target, candidates, msgFormatFn) -> 425 | return unless candidates? 426 | candidatesLow = (s.toLowerCase() for s in candidates) 427 | msgFormatFn candidates[index] if (index = candidatesLow.indexOf target.toLowerCase()) >= 0 428 | 429 | getScoreMatch: (target, candidatesList) -> 430 | # candidatesList is an array of candidates objects. E.g. [{candidates: [], msgFormatFn: ()->}, ...] 431 | # This allows a score match across multiple lists of candidates (e.g. thisMethods and thisProperties) 432 | return unless string_score? 433 | [closestMatch, closestScore, msg] = ['', 0, ''] 434 | for set in candidatesList 435 | if set.candidates? 436 | for match in set.candidates 437 | matchScore = match.score target, scoreFuzziness 438 | [closestMatch, closestScore, msg] = [match, matchScore, set.msgFormatFn(match)] if matchScore > closestScore 439 | msg if closestScore >= acceptMatchThreshold 440 | -------------------------------------------------------------------------------- /src/protectBuiltins.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | problems = require './problems' 4 | 5 | # These builtins, being objects, will have to be cloned and restored. 6 | module.exports.builtinObjectNames = builtinObjectNames = [ 7 | # Built-in Objects 8 | 'Object', 'Function', 'Array', 'String', 'Boolean', 'Number', 'Date', 'RegExp', 'Math', 'JSON', 9 | 10 | # Error Objects 11 | 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError' 12 | ] 13 | 14 | # These builtins aren't objects, so it's easy. 15 | module.exports.builtinNames = builtinNames = builtinObjectNames.concat [ 16 | # Math-related 17 | 'NaN', 'Infinity', 'undefined', 'parseInt', 'parseFloat', 'isNaN', 'isFinite', 18 | 19 | # URI-related 20 | 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 21 | 22 | # Nope! 23 | # 'eval' 24 | ] 25 | 26 | getOwnPropertyNames = Object.getOwnPropertyNames # Grab all properties, including non-enumerable ones. 27 | getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor 28 | defineProperty = Object.defineProperty.bind Object 29 | 30 | globalScope = (-> @)() 31 | builtinClones = [] # We make pristine copies of our builtins so that we can copy them overtop the real ones later. 32 | builtinReal = [] # These are the globals that the player will actually get to mess with, which we'll clean up after. 33 | module.exports.addedGlobals = addedGlobals = {} 34 | 35 | module.exports.addGlobal = addGlobal = (name, value) -> 36 | # Ex.: Aether.addGlobal('Vector', require('lib/world/vector')), before the Aether instance is constructed. 37 | return if addedGlobals[name]? 38 | value ?= globalScope[name] 39 | addedGlobals[name] = value 40 | 41 | addGlobal name for name in builtinObjectNames # Protect our initial builtin objects as globals. 42 | 43 | module.exports.replaceBuiltin = replaceBuiltin = (name, value) -> 44 | #NOOP 45 | -------------------------------------------------------------------------------- /src/ranges.coffee: -------------------------------------------------------------------------------- 1 | module.exports.offsetToPos = offsetToPos = (offset, source, prefix='') -> 2 | rowOffsets = buildRowOffsets source, prefix 3 | offset -= prefix.length 4 | row = offsetToRow offset, rowOffsets 5 | col = offset - rowOffsets[row] 6 | {ofs: offset, row: row, col: col} 7 | 8 | module.exports.offsetsToRange = offsetsToRange = (start, end, source, prefix='') -> 9 | start: offsetToPos(start, source, prefix), end: offsetToPos(end, source, prefix) 10 | 11 | module.exports.rowColToPos = rowColToPos = (row, col, source, prefix='') -> 12 | rowOffsets = buildRowOffsets source, prefix 13 | offset = rowOffsets[row] + col 14 | {ofs: offset, row: row, col: col} 15 | 16 | module.exports.rowColsToRange = rowColsToRange = (start, end, source, prefix='') -> 17 | start: rowColToPos(start.row, start.col, source, prefix), end: rowColToPos(end.row, end.col, source, prefix) 18 | 19 | module.exports.locToPos = locToPos = (loc, source, prefix='') -> 20 | rowColToPos loc.line, loc.column, source, prefix 21 | 22 | module.exports.locsToRange = locsToRange = (start, end, source, prefix='') -> 23 | start: locToPos(start, source, prefix), end: locToPos(end, source, prefix) 24 | 25 | module.exports.stringifyPos = stringifyPos = (pos) -> 26 | "{ofs: #{pos.ofs}, row: #{pos.row}, col: #{pos.col}}" 27 | 28 | module.exports.stringifyRange = stringifyRange = (start, end) -> 29 | "[#{stringifyPos start}, #{stringifyPos end}]" 30 | 31 | # Since we're probably going to be searching the same source many times in a row, 32 | # this simple form of caching should get the job done. 33 | lastRowOffsets = null 34 | lastRowOffsetsSource = null 35 | lastRowOffsetsPrefix = null 36 | buildRowOffsets = (source, prefix='') -> 37 | return lastRowOffsets if source is lastRowOffsetsSource and prefix is lastRowOffsetsPrefix 38 | rowOffsets = [0] 39 | for c, offset in source.substr prefix.length 40 | if c is '\n' 41 | rowOffsets.push offset+1 42 | lastRowOffsets = rowOffsets 43 | lastRowOffsetsSource = source 44 | lastRowOffsetsPrefix = prefix 45 | rowOffsets 46 | 47 | # Fast version using binary search 48 | offsetToRow = (offset, rowOffsets) -> 49 | alen = rowOffsets.length 50 | return 0 if offset <= 0 # First row 51 | return alen - 1 if offset >= rowOffsets[alen - 1] # Last row 52 | lo = 0 53 | hi = alen - 1 54 | while lo < hi 55 | mid = ~~((hi + lo) / 2) # ~~ is a faster, better Math.floor() 56 | return mid if offset >= rowOffsets[mid] and offset < rowOffsets[mid + 1] 57 | if offset < rowOffsets[mid] 58 | hi = mid 59 | else 60 | lo = mid 61 | throw new Error "Bug in offsetToRow()" 62 | -------------------------------------------------------------------------------- /src/transforms.coffee: -------------------------------------------------------------------------------- 1 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 2 | 3 | S = require('esprima').Syntax 4 | 5 | ranges = require './ranges' 6 | 7 | statements = [S.EmptyStatement, S.ExpressionStatement, S.BreakStatement, S.ContinueStatement, S.DebuggerStatement, S.DoWhileStatement, S.ForStatement, S.FunctionDeclaration, S.ClassDeclaration, S.IfStatement, S.ReturnStatement, S.SwitchStatement, S.ThrowStatement, S.TryStatement, S.VariableStatement, S.WhileStatement, S.WithStatement, S.VariableDeclaration] 8 | 9 | getParents = (node) -> 10 | parents = [] 11 | while node.parent 12 | parents.push node = node.parent 13 | parents 14 | 15 | getParentsOfTypes = (node, types) -> 16 | _.filter getParents(node), (elem) -> elem.type in types 17 | 18 | getFunctionNestingLevel = (node) -> 19 | getParentsOfTypes(node, [S.FunctionExpression]).length 20 | 21 | getImmediateParentOfType = (node, type) -> 22 | while node 23 | return node if node.type is type 24 | node = node.parent 25 | 26 | 27 | ########## Before JS_WALA Normalization ########## 28 | 29 | # Original node range preservation. 30 | # 1. Make a many-to-one mapping of normalized nodes to original nodes based on the original ranges, which are unique except for the outer Program wrapper. 31 | # 2. When we generate the normalizedCode, we can also create a source map. 32 | # 3. A postNormalizationTransform can then get the original ranges for each node by going through the source map to our normalized mapping to our original node ranges. 33 | # 4. Instrumentation can then include the original ranges and node source in the saved flow state. 34 | module.exports.makeGatherNodeRanges = makeGatherNodeRanges = (nodeRanges, code, codePrefix) -> (node) -> 35 | return unless node.range 36 | #for x in node.range when _.isNaN x 37 | # console.log "got bad range", node.range, "from", node, node.parent 38 | node.originalRange = ranges.offsetsToRange node.range[0], node.range[1], code, codePrefix 39 | 40 | if node.source 41 | node.originalSource = node.source() 42 | else 43 | #TODO: compute this via ranges 44 | 45 | nodeRanges.push node 46 | 47 | # Making 48 | module.exports.makeCheckThisKeywords = makeCheckThisKeywords = (globals, varNames, language, problemContext) -> 49 | return (node) -> 50 | if node.type is S.VariableDeclarator 51 | varNames[node.id.name] = true 52 | else if node.type is S.AssignmentExpression 53 | varNames[node.left.name] = true 54 | else if node.type is S.FunctionDeclaration or node.type is S.FunctionExpression# and node.parent.type isnt S.Program 55 | varNames[node.id.name] = true if node.id? 56 | varNames[param.name] = true for param in node.params 57 | else if node.type is S.CallExpression 58 | # TODO: false negative when user method call precedes function declaration 59 | v = node 60 | while v.type in [S.CallExpression, S.MemberExpression] 61 | v = if v.object? then v.object else v.callee 62 | v = v.name 63 | if v and not varNames[v] and not (v in globals) 64 | return unless problemContext # If we don't know what properties are available, don't create this problem. 65 | # Probably MissingThis, but let's check if we're recursively calling an inner function from itself first. 66 | for p in getParentsOfTypes node, [S.FunctionDeclaration, S.FunctionExpression, S.VariableDeclarator, S.AssignmentExpression] 67 | varNames[p.id.name] = true if p.id? 68 | varNames[p.left.name] = true if p.left? 69 | varNames[param.name] = true for param in p.params if p.params? 70 | return if varNames[v] is true 71 | return if /\$$/.test v # accum$ in CoffeeScript Redux isn't handled properly 72 | return if problemContext?.thisMethods? and v not in problemContext.thisMethods 73 | # TODO: '@' in CoffeeScript isn't really a keyword 74 | message = "Missing `hero` keyword; should be `#{language.heroValueAccess}#{v}`." 75 | hint = "There is no function `#{v}`, but `hero` has a method `#{v}`." 76 | if node.originalRange 77 | range = language.removeWrappedIndent [node.originalRange.start, node.originalRange.end] 78 | problem = @createUserCodeProblem type: 'transpile', reporter: 'aether', kind: 'MissingThis', message: message, hint: hint, range: range # TODO: code/codePrefix? 79 | @addProblem problem 80 | 81 | module.exports.makeCheckIncompleteMembers = makeCheckIncompleteMembers = (language, problemContext) -> 82 | return (node) -> 83 | # console.log 'check incomplete members', node, node.source() if node.source().search('this.') isnt -1 84 | if node.type is 'ExpressionStatement' 85 | exp = node.expression 86 | if exp.type is 'MemberExpression' 87 | # Handle missing parentheses, like in: this.moveUp; 88 | if exp.property.name is "IncompleteThisReference" 89 | kind = 'IncompleteThis' 90 | m = "this.what? (Check available spells below.)" 91 | hint = '' 92 | else if exp.object.source() is language.thisValue 93 | kind = 'NoEffect' 94 | m = "#{exp.source()} has no effect." 95 | if problemContext?.thisMethods? and exp.property.name in problemContext.thisMethods 96 | m += " It needs parentheses: #{exp.source()}()" 97 | else if problemContext?.commonThisMethods? and exp.property.name in problemContext.commonThisMethods 98 | m = "#{exp.source()} is not currently available." 99 | else 100 | hint = "Is it a method? Those need parentheses: #{exp.source()}()" 101 | if node.originalRange 102 | range = language.removeWrappedIndent [node.originalRange.start, node.originalRange.end] 103 | problem = @createUserCodeProblem type: 'transpile', reporter: 'aether', message: m, kind: kind, hint: hint, range: range # TODO: code/codePrefix? 104 | @addProblem problem 105 | -------------------------------------------------------------------------------- /src/traversal.coffee: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/substack/node-falafel 2 | # A similar approach could be seen in https://github.com/ariya/esmorph 3 | _ = window?._ ? self?._ ? global?._ ? require 'lodash' # rely on lodash existing, since it busts CodeCombat to browserify it--TODO 4 | 5 | # TODO: see about consolidating 6 | module.exports.walkAST = walkAST = (node, fn) -> 7 | for key, child of node 8 | if _.isArray child 9 | for grandchild in child 10 | walkAST grandchild, fn if _.isString grandchild?.type 11 | else if _.isString child?.type 12 | walkAST child, fn 13 | fn child 14 | 15 | module.exports.walkASTCorrect = walkASTCorrect = (node, fn) -> 16 | for key, child of node 17 | if _.isArray child 18 | for grandchild in child 19 | if _.isString grandchild?.type 20 | walkASTCorrect grandchild, fn 21 | else if _.isString child?.type 22 | walkASTCorrect child, fn 23 | fn node 24 | 25 | module.exports.morphAST = morphAST = (source, transforms, parseFn, aether) -> 26 | chunks = source.split '' 27 | ast = parseFn source, aether 28 | 29 | morphWalk = (node, parent) -> 30 | insertHelpers node, parent, chunks 31 | for key, child of node 32 | continue if key is 'parent' or key is 'leadingComments' 33 | if _.isArray child 34 | for grandchild in child 35 | morphWalk grandchild, node if _.isString grandchild?.type 36 | else if _.isString child?.type 37 | morphWalk child, node 38 | transform node, aether for transform in transforms 39 | 40 | morphWalk ast, undefined 41 | chunks.join '' 42 | 43 | insertHelpers = (node, parent, chunks) -> 44 | return unless node.range 45 | node.parent = parent 46 | node.source = -> chunks.slice(node.range[0], node.range[1]).join '' 47 | update = (s) -> 48 | chunks[node.range[0]] = s 49 | for i in [node.range[0] + 1 ... node.range[1]] 50 | chunks[i] = '' 51 | if _.isObject node.update 52 | _.extend update, node.update 53 | node.update = update 54 | -------------------------------------------------------------------------------- /src/validators/options.coffee: -------------------------------------------------------------------------------- 1 | tv4 = require('tv4').tv4 2 | 3 | module.exports = (options) -> 4 | tv4.validateMultiple options, 5 | "type": "object" 6 | additionalProperties: false 7 | properties: 8 | thisValue: 9 | required: false 10 | globals: 11 | type: 'array' 12 | functionName: 13 | type: 'string' 14 | functionParameters: 15 | type: ['array', 'undefined'] 16 | yieldAutomatically: 17 | type: 'boolean' 18 | yieldConditionally: 19 | type: 'boolean' 20 | executionCosts: 21 | type: 'object' 22 | executionLimit: 23 | type: 'integer' 24 | minimum: 0 25 | description: 'If given and non-zero, user code will throw execution exceeded errors after using too many statements.' 26 | language: 27 | type: 'string' 28 | description: "Input language" 29 | minLength:1 30 | 'enum': ['javascript', 'coffeescript', 'python', 'lua', 'java', 'html'] 31 | languageVersion: 32 | type: ['string', 'null', 'undefined'] # TODO: remove option soon 33 | problems: 34 | type: ['object', 'undefined'] 35 | problemContext: 36 | type: ['object', 'null', 'undefined'] 37 | includeFlow: 38 | type: 'boolean' 39 | default: true 40 | description: "Whether to record control flow and variable values as user code executes." 41 | noSerializationInFlow: 42 | type: 'boolean' 43 | default: false 44 | description: "Whether to skip serializing variable values when recording variables in flow." 45 | noVariablesInFlow: 46 | type: 'boolean' 47 | default: false 48 | description: "Whether to skip capturing variable values at all when instrumenting flow." 49 | skipDuplicateUserInfoInFlow: 50 | type: 'boolean' 51 | default: false 52 | description: "Whether to skip recording calls with the same userInfo as the previous call when instrumenting flow." 53 | includeMetrics: 54 | type: 'boolean' 55 | default: true 56 | includeStyle: 57 | type: 'boolean' 58 | default: true 59 | protectAPI: 60 | type: ['boolean', 'null', 'undefined'] 61 | default: false 62 | simpleLoops: 63 | type: 'boolean' 64 | default: false 65 | description: "Whether simple loops will be supported, per language. E.g. 'loop()' will be transpiled as 'while(true)'. Deprecated: simple loops are no longer supported." 66 | protectBuiltins: 67 | type: 'boolean' 68 | default: true 69 | description: 'Whether builtins will be protected and restored for enhanced security.' 70 | whileTrueAutoYield: 71 | type: 'boolean' 72 | default: false 73 | description: "Make while True loops automatically yield if no other yields" 74 | useInterpreter: # TODO: remove option soon 75 | type: ['boolean', 'null', 'undefined'] 76 | default: true 77 | debug: 78 | type: ['boolean'] 79 | default: false 80 | -------------------------------------------------------------------------------- /test/aether_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | describe "Aether", -> 4 | describe "Basic tests", -> 5 | it "doesn't immediately break", -> 6 | aether = new Aether() 7 | code = "var x = 3;" 8 | expect(aether.canTranspile(code)).toEqual true 9 | 10 | it "running functions isn't broken horribly", -> 11 | aether = new Aether() 12 | code = "return 1000;" 13 | aether.transpile(code) 14 | expect(aether.run()).toEqual 1000 15 | 16 | it 'can run an empty function', -> 17 | aether = new Aether() 18 | aether.transpile '' 19 | expect(aether.run()).toEqual undefined 20 | expect(aether.problems.errors).toEqual [] 21 | 22 | describe "Transpile heuristics", -> 23 | aether = null 24 | beforeEach -> 25 | aether = new Aether() 26 | it "Compiles a blank piece of code", -> 27 | raw = "" 28 | expect(aether.canTranspile(raw)).toEqual true 29 | 30 | describe "Defining functions", -> 31 | aether = new Aether() 32 | it "should be able to define functions in functions", -> 33 | code = """ 34 | function fib(n) { 35 | return n < 2 ? n : fib(n - 1) + fib(n - 2); 36 | } 37 | 38 | var chupacabra = fib(6) 39 | return chupacabra; 40 | """ 41 | aether.transpile(code) 42 | fn = aether.createFunction() 43 | expect(fn()).toEqual 8 44 | 45 | describe "Changing Language", -> 46 | aether = new Aether() 47 | it "should change the language if valid", -> 48 | expect(aether.setLanguage "coffeescript").toEqual "coffeescript" 49 | 50 | it "should not allow non-supported languages", -> 51 | expect(aether.setLanguage.bind null, "Brainfuck").toThrow() 52 | 53 | 54 | ### 55 | var test1 = function test2(test3) { 56 | test1(); 57 | test2(); 58 | test3(); 59 | } 60 | test4 = function(test5) { 61 | test4(); 62 | test5(); 63 | } 64 | ### 65 | -------------------------------------------------------------------------------- /test/constructor_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | describe "Constructor Test Suite", -> 4 | describe "Default values", -> 5 | aether = new Aether() 6 | it "should initialize functionName as null", -> 7 | expect(aether.options.functionName).toBeNull() 8 | it "should have javascript as the default language", -> 9 | expect(aether.options.language).toEqual "javascript" 10 | it "should be using ECMAScript 5", -> 11 | expect(aether.options.languageVersion).toBe "ES5" 12 | it "should have no functionParameters", -> 13 | expect(aether.options.functionParameters).toEqual [] 14 | it "should not yield automatically by default", -> 15 | expect(aether.options.yieldAutomatically).toBe false 16 | it "should not yield conditionally", -> 17 | expect(aether.options.yieldConditionally).toBe false 18 | it "should have defined execution costs", -> 19 | expect(aether.options.executionCosts).toBeDefined() 20 | it "should have defined globals", -> 21 | expect(aether.options.globals).toBeDefined() 22 | describe "Custom option values", -> 23 | constructAther = (options) -> 24 | aether = new Aether(options) 25 | beforeEach -> 26 | aether = null 27 | it "should not allow non-supported languages", -> 28 | options = language: "Brainfuck" 29 | expect(constructAther.bind null, options).toThrow() 30 | it "should not allow non-supported language versions", -> 31 | options = languageVersion: "ES7" 32 | expect(constructAther.bind null, options).toThrow() 33 | it "should not allow options that do not exist", -> 34 | options = blah: "blah" 35 | expect(constructAther.bind null, options).toThrow() 36 | -------------------------------------------------------------------------------- /test/cs_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | describe "CS test Suite!", -> 4 | describe "CS compilation", -> 5 | aether = new Aether language: "coffeescript" 6 | it "Should compile functions", -> 7 | code = """ 8 | return 1000 9 | """ 10 | aether.transpile(code) 11 | expect(aether.run()).toEqual 1000 12 | expect(aether.problems.errors).toEqual [] 13 | 14 | describe "CS compilation with lang set after contruction", -> 15 | aether = new Aether() 16 | it "Should compile functions", -> 17 | code = """ 18 | return 2000 if false 19 | return 1000 20 | """ 21 | aether.setLanguage "coffeescript" 22 | aether.transpile(code) 23 | expect(aether.canTranspile(code)).toEqual true 24 | expect(aether.problems.errors).toEqual [] 25 | 26 | describe "CS Test Spec #1", -> 27 | aether = new Aether language: "coffeescript" 28 | it "mathmetics order", -> 29 | code = " 30 | return (2*2 + 2/2 - 2*2/2) 31 | " 32 | aether.transpile(code) 33 | expect(aether.run()).toEqual 3 34 | expect(aether.problems.errors).toEqual [] 35 | 36 | describe "CS Test Spec #2", -> 37 | aether = new Aether language: "coffeescript" 38 | it "function call", -> 39 | code = """ 40 | fib = (n) -> 41 | (if n < 2 then n else fib(n - 1) + fib(n - 2)) 42 | chupacabra = fib(6) 43 | """ 44 | aether.transpile(code) 45 | fn = aether.createFunction() 46 | expect(aether.canTranspile(code)).toEqual true 47 | expect(aether.run()).toEqual 8 # fail 48 | expect(aether.problems.errors).toEqual [] 49 | 50 | describe "Basics", -> 51 | aether = new Aether language: "coffeescript" 52 | it "Simple For", -> 53 | code = """ 54 | count = 0 55 | count++ for num in [1..10] 56 | return count 57 | """ 58 | aether.transpile(code) 59 | expect(aether.canTranspile(code)).toEqual true 60 | expect(aether.run()).toEqual 10 61 | expect(aether.problems.errors).toEqual [] 62 | 63 | it "Simple While", -> 64 | code = """ 65 | count = 0 66 | count++ until count is 100 67 | return count 68 | """ 69 | aether.transpile(code) 70 | expect(aether.canTranspile(code)).toEqual true 71 | expect(aether.run()).toEqual 100 72 | expect(aether.problems.errors).toEqual [] 73 | 74 | it "Should Map", -> 75 | # See: https://github.com/codecombat/aether/issues/97 76 | code = "return (num for num in [10..1])" 77 | 78 | aether.transpile(code) 79 | expect(aether.canTranspile(code)).toEqual true 80 | expect(aether.run()).toEqual [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] 81 | expect(aether.problems.errors).toEqual [] 82 | 83 | it "Should Map properties", -> 84 | code = ''' 85 | yearsOld = max: 10, ida: 9, tim: 11 86 | ages = for child, age of yearsOld 87 | "#{child} is #{age}" 88 | return ages 89 | ''' 90 | aether.transpile(code) 91 | expect(aether.canTranspile(code)).toEqual true 92 | expect(aether.run()).toEqual ["max is 10", "ida is 9", "tim is 11"] 93 | expect(aether.problems.errors).toEqual [] 94 | 95 | it "Should compile empty function", -> 96 | code = """ 97 | func = () -> 98 | return typeof func 99 | """ 100 | aether.transpile(code) 101 | expect(aether.canTranspile(code)).toEqual true 102 | expect(aether.run()).toEqual 'function' 103 | expect(aether.problems.errors).toEqual [] 104 | 105 | it "Should compile objects", -> 106 | code = """ 107 | singers = {Jagger: 'Rock', Elvis: 'Roll'} 108 | return singers 109 | """ 110 | aether.transpile(code) 111 | expect(aether.canTranspile(code)).toEqual true 112 | expect(aether.run()).toEqual ({Jagger: 'Rock', Elvis: 'Roll'}) 113 | expect(aether.problems.errors).toEqual [] 114 | 115 | it "Should compile classes", -> 116 | code = """ 117 | class MyClass 118 | test: -> 119 | return 1000 120 | myClass = new MyClass() 121 | return myClass.test() 122 | """ 123 | aether.transpile(code) 124 | expect(aether.canTranspile(code)).toEqual true 125 | expect(aether.run()).toEqual 1000 126 | expect(aether.problems.errors).toEqual [] 127 | 128 | xit "Should compile super", -> 129 | # super is not supported in CSR yet: https://github.com/michaelficarra/CoffeeScriptRedux/search?q=super&ref=cmdform&type=Issues 130 | code = ''' 131 | class Animal 132 | constructor: (@name) -> 133 | move: (meters) -> 134 | @name + " moved " + meters + "m." 135 | class Snake extends Animal 136 | move: -> 137 | super 5 138 | sam = new Snake "Sammy the Python" 139 | sam.move() 140 | ''' 141 | aether.transpile(code) 142 | expect(aether.run()).toEqual "Sammy the Python moved 5m." 143 | expect(aether.problems.errors).toEqual [] 144 | 145 | it "Should compile string interpolation", -> 146 | code = ''' 147 | meters = 5 148 | "Sammy the Python moved #{meters}m." 149 | ''' 150 | aether.transpile(code) 151 | expect(aether.run()).toEqual "Sammy the Python moved 5m." 152 | expect(aether.problems.errors).toEqual [] 153 | 154 | it "Should implicitly return the last statement", -> 155 | aether.transpile('"hi"') 156 | expect(aether.run()).toEqual 'hi' 157 | expect(aether.problems.errors).toEqual [] 158 | 159 | describe "Errors", -> 160 | aether = new Aether language: "coffeescript" 161 | 162 | it "Bad indent", -> 163 | code = """ 164 | fn = -> 165 | x = 45 166 | x += 5 167 | return x 168 | return fn() 169 | """ 170 | aether.transpile(code) 171 | expect(aether.problems.errors.length).toEqual(1) 172 | # TODO: No range information for this error 173 | # https://github.com/codecombat/aether/issues/114 174 | expect(aether.problems.errors[0].message.indexOf("Syntax error on line 5, column 10: unexpected '+'")).toBe(0) 175 | 176 | it "Transpile error, missing )", -> 177 | code = """ 178 | fn = -> 179 | return 45 180 | x = fn( 181 | return x 182 | """ 183 | aether.transpile(code) 184 | expect(aether.problems.errors.length).toEqual(1) 185 | # TODO: No range information for this error 186 | # https://github.com/codecombat/aether/issues/114 187 | expect(aether.problems.errors[0].message.indexOf("Unexpected DEDENT")).toBe(0) 188 | 189 | xit "Missing @: x() row 0", -> 190 | # TODO: error ranges incorrect 191 | # https://github.com/codecombat/aether/issues/153 192 | code = """x()""" 193 | aether.transpile(code) 194 | expect(aether.problems.errors.length).toEqual(1) 195 | expect(aether.problems.errors[0].message).toEqual("Missing `@` keyword; should be `@x`.") 196 | expect(aether.problems.errors[0].range).toEqual([ { ofs: 0, row: 0, col: 0 }, { ofs: 3, row: 0, col: 3 } ]) 197 | 198 | it "Missing @: x() row 1", -> 199 | code = """ 200 | y = 5 201 | x() 202 | """ 203 | aether.transpile(code) 204 | expect(aether.problems.errors.length).toEqual(1) 205 | expect(aether.problems.errors[0].message).toEqual("Missing `@` keyword; should be `@x`.") 206 | # https://github.com/codecombat/aether/issues/115 207 | # expect(aether.problems.errors[0].range).toEqual([ { ofs: 6, row: 1, col: 0 }, { ofs: 9, row: 1, col: 3 } ]) 208 | 209 | it "Missing @: x() row 3", -> 210 | code = """ 211 | y = 5 212 | s = 'some other stuff' 213 | if y is 5 214 | x() 215 | """ 216 | aether.transpile(code) 217 | expect(aether.problems.errors.length).toEqual(1) 218 | expect(aether.problems.errors[0].message).toEqual("Missing `@` keyword; should be `@x`.") 219 | # https://github.com/codecombat/aether/issues/115 220 | # expect(aether.problems.errors[0].range).toEqual([ { ofs: 42, row: 3, col: 2 }, { ofs: 45, row: 3, col: 5 } ]) 221 | 222 | xit "@getItems missing parentheses", -> 223 | # https://github.com/codecombat/aether/issues/111 224 | history = [] 225 | getItems = -> [{'pos':1}, {'pos':4}, {'pos':3}, {'pos':5}] 226 | move = (i) -> history.push i 227 | thisValue = {getItems: getItems, move: move} 228 | code = """ 229 | @getItems 230 | """ 231 | aether.transpile code 232 | method = aether.createMethod thisValue 233 | aether.run method 234 | expect(aether.problems.errors.length).toEqual(1) 235 | expect(aether.problems.errors[0].message).toEqual('@getItems has no effect.') 236 | expect(aether.problems.errors[0].hint).toEqual('Is it a method? Those need parentheses: @getItems()') 237 | expect(aether.problems.errors[0].range).toEqual([ { ofs : 0, row : 0, col : 0 }, { ofs : 10, row : 0, col : 10 } ]) 238 | 239 | xit "@getItems missing parentheses row 1", -> 240 | # https://github.com/codecombat/aether/issues/110 241 | history = [] 242 | getItems = -> [{'pos':1}, {'pos':4}, {'pos':3}, {'pos':5}] 243 | move = (i) -> history.push i 244 | thisValue = {getItems: getItems, move: move} 245 | code = """ 246 | x = 5 247 | @getItems 248 | y = 6 249 | """ 250 | aether.transpile code 251 | method = aether.createMethod thisValue 252 | aether.run method 253 | expect(aether.problems.errors.length).toEqual(1) 254 | expect(aether.problems.errors[0].message).toEqual('@getItems has no effect.') 255 | expect(aether.problems.errors[0].hint).toEqual('Is it a method? Those need parentheses: @getItems()') 256 | expect(aether.problems.errors[0].range).toEqual([ { ofs : 7, row : 1, col : 0 }, { ofs : 16, row : 1, col : 9 } ]) 257 | 258 | it "Incomplete string", -> 259 | code = """ 260 | s = 'hi 261 | return s 262 | """ 263 | aether.transpile(code) 264 | expect(aether.problems.errors.length).toEqual(1) 265 | expect(aether.problems.errors[0].message).toEqual("Unclosed \"'\" at EOF") 266 | # https://github.com/codecombat/aether/issues/114 267 | # expect(aether.problems.errors[0].range).toEqual([ { ofs : 4, row : 0, col : 4 }, { ofs : 7, row : 0, col : 7 } ]) 268 | 269 | xit "Runtime ReferenceError", -> 270 | # TODO: error ranges incorrect 271 | # https://github.com/codecombat/aether/issues/153 272 | code = """ 273 | x = 5 274 | y = x + z 275 | """ 276 | aether.transpile(code) 277 | aether.run() 278 | expect(aether.problems.errors.length).toEqual(1) 279 | expect(/ReferenceError/.test(aether.problems.errors[0].message)).toBe(true) 280 | expect(aether.problems.errors[0].range).toEqual([ { ofs : 14, row : 1, col : 8 }, { ofs : 15, row : 1, col : 9 } ]) 281 | -------------------------------------------------------------------------------- /test/flow_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | describe "Flow test suite", -> 4 | describe "Basic flow", -> 5 | code = """ 6 | var four = 2 * 2; // three: 2, 2, four = 2 * 2 7 | this.say(four); // one 8 | """ 9 | nStatements = 4 # If we improved our statement detection, this might go up 10 | it "should count statements and track vars", -> 11 | thisValue = say: -> 12 | options = 13 | includeFlow: true 14 | includeMetrics: true 15 | aether = new Aether options 16 | aether.transpile(code) 17 | fn = aether.createMethod thisValue 18 | for i in [0 ... 4] 19 | if i 20 | expect(aether.flow.states.length).toEqual i 21 | expect(aether.flow.states[i - 1].statementsExecuted).toEqual nStatements 22 | expect(aether.metrics.callsExecuted).toEqual i 23 | expect(aether.metrics.statementsExecuted).toEqual i * nStatements 24 | fn() 25 | last = aether.flow.states[3].statements 26 | expect(last[0].variables.four).not.toEqual "4" 27 | expect(last[last.length - 1].variables.four).toEqual "4" # could change if we serialize differently 28 | 29 | it "should obey includeFlow", -> 30 | thisValue = say: -> 31 | options = 32 | includeFlow: false 33 | includeMetrics: true 34 | aether = new Aether options 35 | aether.transpile(code) 36 | fn = aether.createMethod thisValue 37 | fn() 38 | expect(aether.flow.states).toBe undefined 39 | expect(aether.metrics.callsExecuted).toEqual 1 40 | expect(aether.metrics.statementsExecuted).toEqual nStatements 41 | 42 | it "should obey includeMetrics", -> 43 | thisValue = say: -> 44 | options = 45 | includeFlow: true 46 | includeMetrics: false 47 | aether = new Aether options 48 | aether.transpile(code) 49 | fn = aether.createMethod thisValue 50 | fn() 51 | expect(aether.flow.states.length).toEqual 1 52 | expect(aether.flow.states[0].statementsExecuted).toEqual nStatements 53 | expect(aether.metrics.callsExecuted).toBe undefined 54 | expect(aether.metrics.statementsExecuted).toBe undefined 55 | 56 | it "should not log statements when not needed", -> 57 | thisValue = say: -> 58 | options = 59 | includeFlow: false 60 | includeMetrics: false 61 | aether = new Aether options 62 | pure = aether.transpile(code) 63 | expect(pure.search /log(Statement|Call)/).toEqual -1 64 | expect(pure.search /_aetherUserInfo/).toEqual -1 65 | expect(pure.search /_aether\.vars/).toEqual -1 66 | -------------------------------------------------------------------------------- /test/global_scope_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | describe "Global Scope Exploit Suite", -> 4 | # This one should now be handled by strict mode, so this is undefined 5 | it 'should intercept "this"', -> 6 | code = "G=100;var globals=(function(){return this;})();return globals.G;" 7 | aether = new Aether() 8 | aether.transpile(code) 9 | aether.run() 10 | expect(aether.problems.errors.length).toEqual 1 11 | expect(aether.problems.errors[0].message).toMatch /Cannot read property 'G' of undefined/ 12 | 13 | it 'should disallow using eval', -> 14 | code = "eval('var x = 2; ++x;');" 15 | aether = new Aether() 16 | aether.transpile(code) 17 | func = aether.createFunction() 18 | expect(func).toThrow() 19 | 20 | it 'should disallow using eval without identifier', -> 21 | code = "0['ev'+'al']('var x = 2; ++x;');" 22 | aether = new Aether() 23 | aether.transpile(code) 24 | func = aether.createFunction() 25 | expect(func).toThrow() 26 | 27 | it 'should disallow using Function', -> 28 | code = "Function('')" 29 | aether = new Aether() 30 | aether.transpile(code) 31 | func = aether.createFunction() 32 | expect(func).toThrow() 33 | 34 | it 'should disallow Function.__proto__.constructor', -> 35 | code = "(function(){}).__proto__.constructor('')" 36 | aether = new Aether() 37 | aether.transpile(code) 38 | func = aether.createFunction() 39 | expect(func).toThrow() 40 | 41 | it 'should protect builtins', -> 42 | code = "(function(){}).__proto__.constructor = 100;" 43 | aether = new Aether() 44 | aether.transpile(code) 45 | aether.run() 46 | expect((->).__proto__.constructor).not.toEqual 100 47 | 48 | it 'should sandbox nested aether functions', -> 49 | c1 = "arguments[0]();" 50 | c2 = "(function(){}).__proto__.constructor('');" 51 | 52 | aether = new Aether() 53 | aether.transpile c1 54 | f1 = aether.createFunction() 55 | 56 | aether.transpile c2 57 | f2 = aether.createFunction() 58 | 59 | expect(->f1 f2).toThrow() 60 | 61 | it 'shouldn\'t remove sandbox in nested aether functions', -> 62 | c1 = "arguments[0]();(function(){}).__proto__.constructor('');" 63 | c2 = "" 64 | 65 | aether = new Aether() 66 | aether.transpile c1 67 | f1 = aether.createFunction() 68 | 69 | aether.transpile c2 70 | f2 = aether.createFunction() 71 | 72 | expect(->f1 f2).toThrow() 73 | 74 | it 'should sandbox generators', -> 75 | code = "(function(){}).__proto__.constructor();" 76 | aether = new Aether 77 | yieldAutomatically: true 78 | 79 | aether.transpile code 80 | func = aether.sandboxGenerator aether.createFunction()() 81 | 82 | try 83 | while true 84 | func.next() 85 | catch e 86 | # If we change the error message or whatever make sure we change it here too 87 | expect(e.message).toEqual '[Sandbox] Function::constructor is disabled. If you are a developer, please make sure you have a reference to your builtins.' 88 | 89 | it 'should not break on invalid code', -> 90 | code = ''' 91 | var friend = {health: 10}; 92 | if (friend.health < 5) { 93 | this.castRegen(friend); 94 | this.say("Healing " + friend.id + "."); 95 | } 96 | if (this.health < 50) { 97 | ''' 98 | aether = new Aether() 99 | aether.transpile code 100 | fn = aether.createFunction() 101 | fn() 102 | 103 | it 'should protect builtin prototypes', -> 104 | codeOne = ''' 105 | Array.prototype.diff = function(a) { 106 | return this.filter(function(i) { return a.indexOf(i) < 0; }); 107 | }; 108 | var sweet = ["frogs", "toads"]; 109 | var salty = ["toads"]; 110 | return sweet.diff(salty); 111 | ''' 112 | codeTwo = ''' 113 | var a = ["just", "three", "properties"]; 114 | var x = 0; 115 | for (var key in a) 116 | ++x; 117 | return x; 118 | ''' 119 | aether = new Aether() 120 | aether.transpile codeOne 121 | fn = aether.createFunction() 122 | ret = fn() 123 | expect(ret.length).toEqual(1) 124 | 125 | aether = new Aether() 126 | aether.transpile codeTwo 127 | fn = aether.createFunction() 128 | ret = fn() 129 | expect(ret).toEqual 3 130 | expect(Array.prototype.diff).toBeUndefined() 131 | delete Array.prototype.diff # Needed, or test never returns. 132 | 133 | it 'should disallow callee hacking', -> 134 | safe = ["secret"] 135 | music = [] 136 | inner = addMusic: (song) -> music.push song 137 | outer = entertain: -> inner.sing() 138 | outer.entertain.burninate = -> safe.pop() 139 | code = ''' 140 | this.addMusic("trololo"); 141 | var caller = arguments.callee.caller; 142 | this.addMusic("trololo") 143 | var callerDepth = 0; 144 | while (caller.caller && callerDepth++ < 10) { 145 | this.addMusic(''+caller); 146 | caller = caller.caller; 147 | if (caller.burninate) 148 | caller.burninate(); 149 | } 150 | this.addMusic("trololo"); 151 | ''' 152 | aether = new Aether functionName: 'sing' 153 | aether.transpile code 154 | inner.sing = aether.createMethod inner 155 | expect(-> outer.entertain()).toThrow() 156 | expect(safe.length).toEqual 1 157 | expect(music.length).toEqual 1 158 | expect(music[0]).toEqual 'trololo' 159 | 160 | it 'should disallow prepareStackTrace hacking', -> 161 | # https://github.com/codecombat/aether/issues/81 162 | code = """ 163 | var getStackframes = function () { 164 | var capture; 165 | Error.prepareStackTrace = function(e, t) { 166 | return t; 167 | }; 168 | try { 169 | capture.error(); 170 | } catch (e) { 171 | capture = e.stack; 172 | } 173 | return capture; 174 | }; 175 | 176 | var boop = []; 177 | getStackframes().forEach(function(x) { 178 | if(x.getFunctionName() != 'module.exports.Aether.run') 179 | return; 180 | boop.push(x.getFunctionName()); 181 | boop.push(x.getFunction()); 182 | }); 183 | 184 | return boop; 185 | """ 186 | aether = new Aether 187 | aether.transpile code 188 | ret = aether.run() 189 | expect(ret).toEqual null 190 | expect(aether.problems.errors).not.toEqual [] 191 | -------------------------------------------------------------------------------- /test/java_errors_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | lodash = require 'lodash' 3 | 4 | language = 'java' 5 | aether = new Aether language: language 6 | 7 | checkRange = (problem, code, start) -> 8 | if start 9 | expect(problem.range[0].row).toEqual(start.row) 10 | expect(problem.range[0].col).toEqual(start.col) 11 | 12 | xdescribe "#{language} Errors Test suite", -> 13 | describe "Syntax Errors", -> 14 | it "no class", -> 15 | code = """ 16 | hero.moveLeft() 17 | """ 18 | aether = new Aether language: language 19 | aether.transpile code 20 | expect(aether.problems.errors.length).toEqual(1) 21 | expect(aether.problems.errors[0].message).toEqual('class, interface, or enum expected') 22 | checkRange(aether.problems.errors[0], code, {row: 0, col: 0}) 23 | 24 | it "not a statement", -> 25 | code = """ 26 | public class Main { 27 | public static void main(String[] args) { 28 | 2+2; 29 | } 30 | } 31 | """ 32 | aether = new Aether language: language 33 | aether.transpile code 34 | expect(aether.problems.errors.length).toEqual(1) 35 | expect(aether.problems.errors[0].message).toEqual('not a statement') 36 | checkRange(aether.problems.errors[0], code, {row: 3, col: 4}) 37 | 38 | 39 | it "no semicolon", -> 40 | code = """ 41 | public class Main { 42 | public static void main(String[] args) { 43 | hero.moveLeft() 44 | } 45 | } 46 | """ 47 | aether = new Aether language: language 48 | aether.transpile code 49 | expect(aether.problems.errors.length).toEqual(1) 50 | expect(aether.problems.errors[0].message).toEqual("';' expected") 51 | checkRange(aether.problems.errors[0], code, {row: 3, col: 19}) 52 | 53 | it "space instead of peroid in call", -> 54 | code = """ 55 | public class Main { 56 | public static void main(String[] args) { 57 | hero moveLeft() 58 | } 59 | } 60 | """ 61 | aether = new Aether language: language 62 | aether.transpile code 63 | expect(aether.problems.errors.length).toEqual(1) 64 | expect(aether.problems.errors[0].message).toEqual("';' expected") 65 | checkRange(aether.problems.errors[0], code, {row: 3, col: 5}) 66 | 67 | it "unclosed comment", -> 68 | code = """ 69 | public class Main { 70 | public static void main(String[] args) { 71 | /* 72 | } 73 | } 74 | """ 75 | aether = new Aether language: language 76 | aether.transpile code 77 | expect(aether.problems.errors.length).toEqual(1) 78 | expect(aether.problems.errors[0].message).toEqual("reached end of file while parsing") 79 | checkRange(aether.problems.errors[0], code, {row: 3, col: 5}) 80 | 81 | it "unclosed if", -> 82 | code = """ 83 | public class Main { 84 | public static void main(String[] args) { 85 | hero.moveLeft(); 86 | if ( true ) { 87 | hero.moveRight(); 88 | } 89 | } 90 | """ 91 | aether = new Aether language: language 92 | aether.transpile code 93 | expect(aether.problems.errors.length).toEqual(1) 94 | expect(aether.problems.errors[0].message).toEqual("reached end of file while parsing") 95 | checkRange(aether.problems.errors[0], code, {row: 6, col: 1}) 96 | 97 | it "dangeling type", -> 98 | code = """ 99 | public class Main { 100 | public static void main(String[] args) { 101 | hero.moveLeft(); 102 | int; 103 | } 104 | } 105 | """ 106 | aether = new Aether language: language 107 | aether.transpile code 108 | expect(aether.problems.errors.length).toEqual(1) 109 | expect(aether.problems.errors[0].message).toEqual("not a statement") 110 | checkRange(aether.problems.errors[0], code, {row: 4, col: 5}) 111 | 112 | it "no method", -> 113 | code = """ 114 | public class Main { 115 | moveLeft() 116 | } 117 | """ 118 | aether = new Aether language: language 119 | aether.transpile code 120 | expect(aether.problems.errors.length).toEqual(1) 121 | expect(aether.problems.errors[0].message).toEqual("invalid method declaration; return type required") 122 | checkRange(aether.problems.errors[0], code, {row: 3, col: 3}) -------------------------------------------------------------------------------- /test/java_milestones_spec.ec5: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var cp = require('child_process'); 3 | 4 | var testTheTests = false; 5 | var insideAether = true; 6 | 7 | try { 8 | require("../aether.js") 9 | } catch ( e ) { 10 | insideAether = false; 11 | } 12 | 13 | xdescribe('Java Milestones', function() { 14 | 15 | if ( false ) { 16 | var x = describe; 17 | describe = function( ) { } 18 | var fdescribe = x 19 | } else { 20 | var fdescribe = describe; 21 | } 22 | 23 | if ( !insideAether ) { 24 | var escodegen = require('escodegen'); 25 | var cashew = require('../../cashew.js'); 26 | var evaluateJS =function (type, body, extra) { 27 | var buffer = ""; 28 | cashew.___JavaRuntime.functions.print = function(what) { 29 | buffer += what + "\n"; 30 | } 31 | var code = [ 32 | 'import java.util.*;', 33 | 'public class MyClass {', 34 | ' ' + extra.join("\n\t"), 35 | ' public static ' + type + ' output() {', 36 | ' ' + body.join("\n\t\t"), 37 | ' }', 38 | '}' 39 | ]; 40 | var Cashew = cashew.Cashew; 41 | var parsedAST = Cashew(code.join("\n")); 42 | var js = escodegen.generate(parsedAST); 43 | js = "(function(___JavaRuntime){" + js + "return MyClass.output();})(cashew.___JavaRuntime);"; 44 | var out = eval(js); 45 | return buffer + out; 46 | } 47 | } else { 48 | var Aether = require('../aether.js'); 49 | var cashew = require('cashew-js'); 50 | var evaluateJS = function (type, body, extra) { 51 | buffer = ""; 52 | var a = new Aether({language: 'java'}); 53 | cashew.___JavaRuntime.functions.print = function(what) { 54 | buffer += what + "\n"; 55 | } 56 | var code = [ 57 | 'import java.util.*;', 58 | 'public class MyClass {', 59 | ' ' + extra.join("\n\t"), 60 | ' public static ' + type + ' output() {', 61 | ' ' + body.join("\n\t\t"), 62 | ' }', 63 | '}' 64 | ]; 65 | a.className = "MyClass"; 66 | a.staticCall = "output"; 67 | a.transpile(code.join("\n")); 68 | var out = a.run(); 69 | //console.log(code.join("\n"), out); 70 | return buffer + out; 71 | } 72 | } 73 | 74 | function evaluateJava(type, body, extra) { 75 | var code = [ 76 | 'import java.util.*;', 77 | 'public class MyClass {', 78 | ' ' + extra.join("\n\t"), 79 | ' public static ' + type + ' output() {', 80 | ' ' + body.join("\n\t\t"), 81 | ' }', 82 | ' public static void main(String[] args) {', 83 | ' System.out.print(output());', 84 | ' }', 85 | '}' 86 | ]; 87 | //console.log(code.join("\n")); 88 | if ( fs.existsSync('MyClass.java') ) fs.unlinkSync('MyClass.java'); 89 | if ( fs.existsSync('MyClass.class') ) fs.unlinkSync('MyClass.class'); 90 | fs.writeFileSync('MyClass.java', code.join("\n")); 91 | cp.execSync('javac MyClass.java'); 92 | var out = cp.execSync('java MyClass'); 93 | return out.toString(); 94 | 95 | 96 | } 97 | 98 | function ct(desc, type, code, extra, expected) { 99 | if ( expected == undefined ) { 100 | expected = extra; 101 | extra = []; 102 | } 103 | 104 | it(desc, function() { 105 | if ( testTheTests ) { 106 | var test = evaluateJava(type, code, extra).toString(); 107 | } else { 108 | var test = evaluateJS(type, code, extra).toString(); 109 | } 110 | expect(test).not.toBeNull(); 111 | expect(test).toEqual(expected); 112 | //console.log(desc, test,"\t",expected); 113 | }); 114 | } 115 | 116 | function cts(type, code, expected) { 117 | ct(code, type, [code], expected); 118 | } 119 | 120 | function structureIf(cond, expected) { 121 | ct(cond, 'String', [ 122 | 'if (' + cond + ' ) return "Positive";', 123 | 'return "Negitive";' 124 | ], expected); 125 | } 126 | 127 | 128 | 129 | describe('JAVAM - 05 - primative types', function() { 130 | cts('int', 'return 1000;', '1000'); 131 | cts('String', 'return "Hello World";', 'Hello World'); 132 | cts('boolean', 'return true;', 'true'); 133 | cts('double', 'return 3.14159;', '3.14159'); 134 | }); 135 | 136 | describe('JAVAM - 05 - assignment', function() { 137 | ct('int', 'int', ['int x = 10;', 'return x;'], '10') 138 | ct('String', 'String', ['String str = "Four score and seven years ago.";', 'return str;'], 'Four score and seven years ago.') 139 | ct('boolean', 'boolean', ['boolean truthyness = true;', 'return truthyness;'], 'true') 140 | ct('double', 'double', ['double roar = 17.76;', 'return roar;'], '17.76') 141 | }); 142 | 143 | describe('JAVAM - 05 - logical operators', function() { 144 | cts('boolean', 'return true && true;', 'true'); 145 | cts('boolean', 'return true && false;', 'false'); 146 | cts('boolean', 'return false && true;', 'false'); 147 | cts('boolean', 'return false && false;', 'false'); 148 | cts('boolean', 'return true || true;', 'true'); 149 | cts('boolean', 'return true || false;', 'true'); 150 | cts('boolean', 'return false || true;', 'true'); 151 | cts('boolean', 'return false || false;', 'false'); 152 | }); 153 | 154 | describe('JAVAM - 05 - math operators', function() { 155 | ct('addition', 'int', ['return 2+2;'], '4'); 156 | ct('easy int division', 'int', ['return 6/2;'], '3'); 157 | ct('round-down int division', 'int', ['return 5/2;'], '2'); 158 | ct('double division', 'double', ['return 5.0/2;'], '2.5'); 159 | 160 | cts('int', 'return 2+2*3;', '8'); 161 | cts('int', 'return 7%3;', '1'); 162 | cts('int', 'return -7%3;', '-1'); 163 | cts('int', 'return -7%-3;', '-1'); 164 | cts('int', 'return 7%-3;', '1'); 165 | }); 166 | 167 | describe('JAVAM - 05 - string concatenation', function() { 168 | ct('simple', 'String', ['return "Hello" + " " + "World";'], 'Hello World'); 169 | }); 170 | 171 | describe('JAVAM - 05 - if statements', function() { 172 | structureIf('true', 'Positive'); 173 | structureIf('false', 'Negitive'); 174 | structureIf('true && false', 'Negitive'); 175 | }); 176 | 177 | describe('JAVAM - 05 - for loops', function() { 178 | ct('Add 0..5', 'int', [ 179 | 'int sum = 0;', 180 | 'for ( int i = 0; i < 6; ++i ) {', 181 | ' sum += i;', 182 | '}', 183 | 'return sum;' 184 | ],"" + (1+2+3+4+5)) 185 | 186 | ct('No init', 'int', [ 187 | 'int sum = 0;', 188 | 'for ( ; sum < 123; sum = sum * 2 ) {', 189 | ' sum = sum + 1;', 190 | '}', 191 | 'return sum;' 192 | ],"126") 193 | 194 | ct('break', 'int', [ 195 | 'int sum = 0;', 196 | 'for ( int i = 0; i < 10; ++i ) {', 197 | ' sum = sum + 1;', 198 | ' break;', 199 | '}', 200 | 'return sum;' 201 | ],"1") 202 | 203 | }); 204 | 205 | describe('JAVAM - 05 - while loops', function() { 206 | ct('Add 0..5', 'int', [ 207 | 'int sum = 0;', 208 | 'int i = 0;', 209 | 'while ( i < 6 ) {', 210 | ' sum += i;', 211 | ' i = i + 1;', 212 | '}', 213 | 'return sum;' 214 | ],"" + (1+2+3+4+5)); 215 | 216 | ct('break', 'int', [ 217 | 'int sum = 0;', 218 | 'int i = 1;', 219 | 'while ( i < 6 ) {', 220 | ' sum += i;', 221 | ' i = i + 1;', 222 | ' break;', 223 | '}', 224 | 'return sum;' 225 | ],"1"); 226 | 227 | ct('continue', 'int', [ 228 | 'int sum = 0;', 229 | 'int i = 0;', 230 | 'while ( i < 6 ) {', 231 | ' i = i + 1;', 232 | ' if ( i % 2 == 1 ) continue;', 233 | ' sum += i;', 234 | '}', 235 | 'return sum;' 236 | ],"" + (2+4+6)); 237 | 238 | }); 239 | 240 | describe('JAVAM - 05 - SystemOut', function() { 241 | cts('boolean', 'System.out.println("Hello!"); return true;', "Hello!\ntrue"); 242 | cts('boolean', 'System.out.println(10); return true;', "10\ntrue"); 243 | cts('boolean', 'System.out.print("Hello!"); return true;', "Hello!true"); 244 | }); 245 | 246 | describe('JAVAM - 07 - class definition', function() { 247 | 248 | 249 | }); 250 | 251 | describe('JAVAM - 07 - 2d arrays', function() { 252 | ct('simple', 'int', [ 253 | 'int[][] multi = new int[5][10];', 254 | 'multi[0] = new int[10];', 255 | 'multi[1] = new int[10];', 256 | 'multi[2] = new int[10];', 257 | 'multi[3] = new int[10];', 258 | 'multi[4] = new int[10];', 259 | 'multi[1][4] = 3;', 260 | 'return multi[1][4];' 261 | ], "3"); 262 | 263 | ct('initial matrix', 'String', [ 264 | 'int[][] multi = new int[][] {', 265 | ' { 1, 0, 0, 0, 0, 0, 0, 0, 9, 0 },', 266 | ' { 0, 2, 7, 0, 0, 0, 0, 8, 0, 0 },', 267 | ' { 0, 0, 3, 0, 0, 0, 7, 0, 0, 0 },', 268 | ' { 0, 0, 0, 4, 0, 6, 0, 0, 0, 0 },', 269 | ' { 0, 0, 0, 0, 5, 0, 0, 0, 0, 0 }', 270 | '};', 271 | 'return "-> " + multi[1][2] + " " + multi[4][4];' 272 | ], '-> 7 5'); 273 | 274 | }); 275 | 276 | describe('JAVAM - 07 - ternary operator', function() { 277 | cts('String', 'return true ? "A" : "B";', "A"); 278 | cts('String', 'return false ? "A" : "B";', "B"); 279 | cts('String', 'return 2+2 == 4 ? "A" : "B";', "A"); 280 | cts('String', 'return 2+2 == 5 ? "A" : "B";', "B"); 281 | }); 282 | 283 | describe('JAVAM - 07 - switch', function() { 284 | ct('basic switch', 'String', [ 285 | 'int x = 3;', 286 | 'switch ( x ) {', 287 | ' case 1: return "No";', 288 | ' case 2: return "No";', 289 | ' case 3: return "Yes";', 290 | ' case 4: return "No";', 291 | ' default: return "No";', 292 | '}' 293 | ], "Yes"); 294 | 295 | ct('fall though switch', 'String', [ 296 | 'int x = 3;', 297 | 'switch ( x ) {', 298 | ' case 1: ', 299 | ' case 2: return "No";', 300 | ' case 3: ', 301 | ' case 4: return "Yes";', 302 | ' default: return "No";', 303 | '}' 304 | ], "Yes"); 305 | 306 | ct('default switch', 'String', [ 307 | 'int x = 8;', 308 | 'switch ( x ) {', 309 | ' case 1: return "No";', 310 | ' case 2: return "No";', 311 | ' case 3: return "No";', 312 | ' case 4: return "No";', 313 | ' default: return "Yes";', 314 | '}' 315 | ], "Yes"); 316 | 317 | ct('case into default switch', 'String', [ 318 | 'int x = 4;', 319 | 'switch ( x ) {', 320 | ' case 1: ', 321 | ' case 2: ', 322 | ' case 3: return "No";', 323 | ' case 4:', 324 | ' default: return "Yes";', 325 | '}' 326 | ], "Yes"); 327 | 328 | ct('breaking switch', 'String', [ 329 | 'int x = 1;', 330 | 'switch ( x ) {', 331 | ' case 1: ', 332 | ' case 2: ', 333 | ' case 3: break;', 334 | ' case 4:', 335 | ' default: return "No";', 336 | '}', 337 | 'return "Yes";' 338 | ], "Yes"); 339 | 340 | ct('keep falling', 'String', [ 341 | 'int x = 2;', 342 | 'String r = "";', 343 | 'switch ( x ) {', 344 | ' case 1: r += "No";', 345 | ' case 2: r += "A";', 346 | ' case 3: r += "B";', 347 | ' case 4: r += "C";', 348 | ' default: return r + "Yes";', 349 | '}' 350 | ], "ABCYes"); 351 | 352 | }); 353 | 354 | describe('JAVAM - 07 - complex assignment', function() { 355 | ct('+= (int)', 'int', ['int x = 7;','x += 2;','return x;'], '9'); 356 | ct('-= (int)', 'int', ['int x = 7;','x -= 2;','return x;'], '5'); 357 | ct('*= (int)', 'int', ['int x = 7;','x *= 2;','return x;'], '14'); 358 | ct('/= (int)', 'int', ['int x = 7;','x /= 2;','return x;'], '3'); 359 | ct('%= (int)', 'int', ['int x = 7;','x %= 2;','return x;'], '1'); 360 | 361 | ct('+= (int v)', 'int', ['int x = 7;','int y = 5;', 'x += y;','return x;'], '12'); 362 | ct('*= (int v)', 'int', ['int x = 7;','int y = 5;', 'x *= y;','return x;'], '35'); 363 | ct('/= (int v)', 'int', ['int x = 7;','int y = 5;', 'x /= y;','return x;'], '1'); 364 | ct('/= (d,i v)', 'double', ['double x = 7;','int y = 2;', 'x /= y;','return x;'], '3.5'); 365 | 366 | }); 367 | 368 | 369 | describe('JAVAM - 07 - increment', function() { 370 | ct('++x (int)', 'int', ['int x = 7;','++x;','return x;'], '8'); 371 | ct('--x (int)', 'int', ['int x = 7;','--x;','return x;'], '6'); 372 | ct('x++ (int)', 'int', ['int x = 7;','x++;','return x;'], '8'); 373 | ct('x-- (int)', 'int', ['int x = 7;','x--;','return x;'], '6'); 374 | 375 | ct('++x (double)', 'double', ['double x = 7.5;','++x;','return x;'], '8.5'); 376 | ct('--x (double)', 'double', ['double x = 7.5;','--x;','return x;'], '6.5'); 377 | ct('x++ (double)', 'double', ['double x = 7.5;','x++;','return x;'], '8.5'); 378 | ct('x-- (double)', 'double', ['double x = 7.5;','x--;','return x;'], '6.5'); 379 | 380 | }); 381 | 382 | 383 | describe('JAVAM - 07 - bitwise operators', function() { 384 | cts('int', 'return 20<<3;', '160'); 385 | cts('int', 'return 20>>3;', '2'); 386 | cts('int', 'return -20<<3;', '-160'); 387 | cts('int', 'return 20<<-3;', '-2147483648'); 388 | cts('int', 'return 20>>-3;', '0'); 389 | cts('int', 'return ~20;', '-21'); 390 | }); 391 | 392 | describe('JAVAM - 07 - complex if statements', function() { 393 | structureIf('2+2 == 4', 'Positive'); 394 | structureIf('2+2 == 5', 'Negitive'); 395 | structureIf('2+2 != 4', 'Negitive'); 396 | structureIf('2+2 != 5', 'Positive'); 397 | }); 398 | 399 | describe('JAVAM - 07 - braceless if', function() { 400 | ct('braceless', 'String',[ 401 | 'if ( 1 == 2 ) return "Wrong";', 402 | 'else return "Right";' 403 | ], 'Right') 404 | }); 405 | 406 | describe('JAVAM - 07 - instance/static variables', function() { 407 | var modifiers = ['public', 'private', '']; 408 | for ( var o in modifiers ) { 409 | var m = modifiers[o]; 410 | ct(m + ' static int', 'int',[ 411 | 'return x;' 412 | ],[ 413 | m + ' static int x = 10;' 414 | ], '10') 415 | } 416 | 417 | 418 | }); 419 | 420 | describe('JAVAM - 07 - static class method invocation', function() { 421 | ct('simple method', 'String', [ 422 | 'return getString();' 423 | ],[ 424 | 'public static String getString() {', 425 | ' return "A String";', 426 | '}' 427 | ], "A String"); 428 | }); 429 | 430 | // Testable methods from Apendix a 431 | describe('JAVAM - 09 - String class', function () { 432 | var extra = [ 433 | 'public static int x(int o) {', 434 | ' if ( o == 0 ) return 0;', 435 | ' else if ( o > 0 ) return 1;', 436 | ' else return -1;', 437 | '}' 438 | ] 439 | ct('compareTo', 'int', ['return x("Apple".compareTo("Boat"));'], extra, '-1'); 440 | ct('compareTo', 'int', ['return x("Boat".compareTo("Boat"));'], extra, '0'); 441 | ct('compareTo', 'int', ['return x("apple".compareTo("Boat"));'], extra, '1'); 442 | ct('compareTo', 'int', ['return x("apple".compareToIgnoreCase("Boat"));'], extra, '-1'); 443 | cts('int', 'String s = "capital"; return s.length();', '7'); 444 | cts('int', 'return "capital".length();', '7'); 445 | cts('int', 'return "".length();', '0'); 446 | cts('int', 'String s = "hello"; return s.length();', '5'); 447 | cts('String', 'return "capital".substring(3);', 'ital'); 448 | cts('String', 'return "capital".substring(2,4);', 'pi'); 449 | cts('int', 'return "capital".indexOf("it");', '3'); 450 | cts('int', 'return "capital".indexOf("rob");', '-1'); 451 | }) 452 | 453 | describe('JAVAM - 09 - operator overloading', function() { 454 | var methods = [ 455 | 'public static int multi(int x) { return 1; }', 456 | 'public static int multi(double x) { return 2; }', 457 | 'public static int multi(int x, double y) { return 3; }', 458 | 'public static double multi(double x, int y) { return 4; }', 459 | 'public static double three(int x) { return x * 2; }', 460 | 'public static double three(double x) { return x * 3; }', 461 | 'public static double three(double x, int y) { return x * 4; }', 462 | ] 463 | 464 | ct('1', 'int', ['return multi(2);'], methods, '1'); 465 | ct('2', 'int', ['return multi(2.0);'], methods, '2'); 466 | ct('3', 'int', ['return multi(2, 2.0);'], methods, '3'); 467 | ct('4', 'double', ['return 16 / multi((double) 1, 2);'], methods, '4.0'); 468 | ct('5',' double', ['return three(7);'], methods, '14.0'); 469 | ct('6',' double', ['return three(7, 2);'], methods, '28.0'); 470 | }); 471 | 472 | 473 | 474 | describe('JAVAM - 09 - Constructors ', function() { 475 | var extra = [ 476 | 'private int x = 1;', 477 | 'public MyClass(int y) {', 478 | ' super();', 479 | ' x = y * 2;', 480 | '}', 481 | 'public MyClass() { super(); }', 482 | 'public int z() { return x; }', 483 | 'public int w(int x) { return x; }' 484 | ]; 485 | ct('1', 'int', ['return new MyClass().z();'], extra, '1'); 486 | ct('2', 'int', ['return new MyClass(2).z();'], extra, '4'); 487 | ct('3', 'int', ['return new MyClass().w(5);'], extra, '5'); 488 | ct('4', 'int', ['return new MyClass(2).w(6);'], extra, '6'); 489 | }); 490 | 491 | describe('JAVAM - 09 - Object as SuperClass ', function() { 492 | ct('equals', 'int', [ 493 | 'MyClass c = new MyClass();', 494 | 'if ( c.equals(c) ) return 1;', 495 | 'return 0;', 496 | ], '1') 497 | ct('equals', 'int', [ 498 | 'MyClass c = new MyClass();', 499 | 'if ( c.equals(new MyClass()) ) return 1;', 500 | 'return 0;', 501 | ], '0') 502 | ct('equals', 'String', [ 503 | 'return new MyClass().toString();' 504 | ], [ 505 | 'public String toString() {', 506 | 'return "!" + super.toString().substring(0, 7);', 507 | '}', 508 | ],'!MyClass') 509 | }); 510 | 511 | describe('JAVAM - 09 - Autoboxing ', function() { 512 | var extra = [ 513 | 'public static int a(Integer i) {', 514 | ' return 2 * i.intValue();', 515 | '}', 516 | 'public static int a(Object o) {', 517 | ' Integer i = (Integer) o;', 518 | ' return 3 * i.intValue();', 519 | '}', 520 | 'public static int b(Object o) {', 521 | ' Integer i = (Integer) o;', 522 | ' return 5 * i.intValue();', 523 | '}', 524 | 'public static String c(Object o) {', 525 | ' return o.toString();', 526 | '}', 527 | 'public static double d(Double i) {', 528 | ' return 2 * i.intValue();', 529 | '}', 530 | 'public static double d(Object o) {', 531 | ' Double d = (Double) o;', 532 | ' return 3 * d.intValue();', 533 | '}', 534 | 'public static double e(Object o) {', 535 | ' Double d = (Double) o;', 536 | ' return 3 * d.intValue();', 537 | '}', 538 | ]; 539 | ct('intValue', 'int', ['return a(10);'], extra, '20'); 540 | ct('intValue', 'int', ['return a((Object)10);'], extra, '30'); 541 | ct('intValueCasting', 'int', ['return b(20);'], extra, '100'); 542 | ct('intString', 'String', ['return c(8);'], extra, '8'); 543 | 544 | ct('doubleValue', 'double', ['return d(10.0);'], extra, '20.0'); 545 | ct('doubleValue', 'double', ['return d((Object)10.0);'], extra, '30.0'); 546 | ct('doubleValueCasting', 'double', ['return e(20.0);'], extra, '60.0'); 547 | ct('doubleString', 'String', ['return c(8.0);'], extra, '8.0'); 548 | 549 | }); 550 | 551 | 552 | 553 | describe('JAVAM - 09 - Scoping ', function() { 554 | var extra = [ 555 | 'public static int x = -100;' 556 | ]; 557 | ct('findStaticVariable', 'int', ['return x;'], extra, '-100'); 558 | ct('findLocal', 'int', [ 559 | 'int x = 10;', 560 | 'return x;' 561 | ], extra, '10'); 562 | ct('findStaticWithClassName', 'int', [ 563 | 'int x = 10;', 564 | 'x++;', 565 | 'return MyClass.x;' 566 | ], extra, '-100'); 567 | ct('forDoesntClobberStatic', 'int', [ 568 | 'int c = 0;', 569 | 'for ( int x = 0; x < 10; ++x ) {', 570 | ' ++c;', 571 | '}', 572 | 'return x;' 573 | ], extra, '-100'); 574 | ct('forClobbersStaicInScope', 'int', [ 575 | 'int c = 0;', 576 | 'for ( int x = 0; x < 10; ++x ) {', 577 | ' return x;', 578 | '}', 579 | 'return -200;' 580 | ], extra, '0'); 581 | }); 582 | 583 | describe('JAVAM - 09 - Type Chaining', function() { 584 | var extra = [ 585 | 'public static int a(double c) { return (int)(7.0*c); }', 586 | 'public static String a(int x) { return "Hello" + x; }', 587 | 'public static double a(String x) { return 8.0 * x.length(); }' 588 | ]; 589 | 590 | ct('1a', 'int', ['return a(1.0);' ], extra, '7'); 591 | ct('1b', 'String', ['return a(1);' ], extra, 'Hello1'); 592 | ct('1c', 'double', ['return a("rob");' ], extra, '24.0'); 593 | ct('1d', 'String', ['return a("rob".length());' ], extra, 'Hello3'); 594 | 595 | ct('2a', 'String', ['return a(a(1.0));' ], extra, 'Hello7'); 596 | ct('2b', 'double', ['return a(a(1));' ], extra, '48.0'); 597 | ct('2c', 'int', ['return a(a("rob"));' ], extra, '168'); 598 | ct('2d', 'double', ['return a(a("rob".length()));' ], extra, '48.0'); 599 | 600 | ct('3a', 'double', ['return a(a(a(1.0)));' ], extra, '48.0'); 601 | ct('3b', 'int', ['return a(a(a(1)));' ], extra, '336'); 602 | ct('3c', 'String', ['return a(a(a("rob")));' ], extra, 'Hello168'); 603 | ct('3d', 'int', ['return a(a(a("rob".length())));' ], extra, '336'); 604 | 605 | }); 606 | 607 | describe('JAVAM - 09 - Math', function() { 608 | ct('abs', 'int', ['return Math.abs(10);'], '10'); 609 | ct('abs', 'int', ['return Math.abs(-10);'], '10'); 610 | ct('pow', 'double', ['return Math.pow(2,8);'], '256.0'); 611 | ct('pow', 'double', ['return Math.pow(16,0.5);'], '4.0'); 612 | ct('sqrt', 'double', ['return Math.sqrt(16);'], '4.0'); 613 | ct('sqrt', 'double', ['return Math.sqrt(-4);'], 'NaN'); 614 | ct('random', 'int', ['return (int)Math.random();'], '0'); 615 | }); 616 | 617 | describe('JAVAM - 09 - List', function() { 618 | function w(l) { 619 | return [ 620 | 'List list = new ArrayList();', 621 | 'list.add(8);', //0 622 | 'list.add(6);', //1 623 | 'list.add(7);', //2 624 | 'list.add(5);', //3 625 | 'list.add(3);', //4 626 | 'list.add(0);', //5 627 | 'list.add(9);', //6 628 | ].concat(l); 629 | } 630 | 631 | ct('size', 'int', w([ 632 | 'return list.size();' 633 | ]), '7'); 634 | 635 | ct('get', 'int', w([ 636 | 'return list.get(3);' 637 | ]), '5'); 638 | 639 | ct('set', 'int', w([ 640 | 'return list.set(0,20);' 641 | ]), '8'); 642 | 643 | ct('set', 'int', w([ 644 | 'list.set(0,20);', 645 | 'return list.get(0);' 646 | ]), '20'); 647 | 648 | ct('remove a', 'int', w([ 649 | 'return list.remove(5);' 650 | ]), '0'); 651 | 652 | ct('remove b', 'int', w([ 653 | 'list.remove(3);', 654 | 'return list.size();', 655 | ]), '6'); 656 | 657 | ct('remove c', 'int', w([ 658 | 'list.remove(3);', 659 | 'return list.get(3);', 660 | ]), '3'); 661 | 662 | ct('remove d', 'int', w([ 663 | 'list.remove(3);', 664 | 'return list.get(1);', 665 | ]), '6'); 666 | 667 | ct('remove e', 'int', w([ 668 | 'list.remove(3);', 669 | 'return list.get(5);', 670 | ]), '9'); 671 | }) 672 | 673 | describe('JAVAM - 09 - For Each', function() { 674 | ct('from array', 'int', [ 675 | 'int[] nums = {8, 6, 7, 5, 3, 0, 9};', 676 | 'int sum = 0;', 677 | 'for ( int n : nums ) {', 678 | ' sum += n;', 679 | '}', 680 | 'return sum;' 681 | ], '38'); 682 | 683 | ct('from list unboxing', 'int', [ 684 | 'List list = new ArrayList();', 685 | 'list.add(8);', 686 | 'list.add(6);', 687 | 'list.add(7);', 688 | 'list.add(5);', 689 | 'list.add(3);', 690 | 'list.add(0);', 691 | 'list.add(9);', 692 | 'int sum = 0;', 693 | 'for ( int n : list ) {', 694 | ' sum += n;', 695 | '}', 696 | 'return sum;' 697 | ], '38'); 698 | 699 | ct('from list no-unboxing', 'int', [ 700 | 'List list = new ArrayList();', 701 | 'list.add(new Integer(8));', 702 | 'list.add(new Integer(6));', 703 | 'list.add(new Integer(7));', 704 | 'list.add(new Integer(5));', 705 | 'list.add(new Integer(3));', 706 | 'list.add(new Integer(0));', 707 | 'list.add(new Integer(9));', 708 | 'int sum = 0;', 709 | 'for ( Integer n : list ) {', 710 | ' sum += n.intValue();', 711 | '}', 712 | 'return sum;' 713 | ], '38'); 714 | 715 | }); 716 | 717 | 718 | 719 | }); -------------------------------------------------------------------------------- /test/java_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | xdescribe "Java test suite", -> 4 | describe "Java Basics", -> 5 | aether = new Aether language: "java" 6 | it "05 - JAVA - return 1000", -> 7 | code = """ 8 | public class MyClass{ 9 | public static int output() 10 | { 11 | return 1000; 12 | } 13 | } 14 | """ 15 | aether.className = "MyClass" 16 | aether.staticCall = "output" 17 | aether.transpile(code) 18 | expect(aether.run()).toEqual 1000 19 | 20 | it "05 - JAVA - variable", -> 21 | aether = new Aether language: "java" 22 | code = """ 23 | public class MyClass 24 | { 25 | public static String output() 26 | { 27 | int x = 10; 28 | return x; 29 | } 30 | } 31 | """ 32 | aether.className = "MyClass" 33 | aether.staticCall = "output" 34 | aether.transpile(code) 35 | expect(aether.run()).toEqual 10 36 | 37 | it "05 - JAVA - Logical operators", -> 38 | aether = new Aether language: "java" 39 | code = """ 40 | public class LogicalClass 41 | { 42 | public static String output() 43 | { 44 | boolean testTrue = true; 45 | boolean testFalse = false; 46 | if(testTrue && testFalse){ 47 | return "Print not Expected"; 48 | }else{ 49 | return "Print Expected"; 50 | } 51 | } 52 | } 53 | """ 54 | aether.className = "LogicalClass" 55 | aether.staticCall = "output" 56 | aether.transpile(code) 57 | expect(aether.run()).toBe('Print Expected') 58 | 59 | it "05 - JAVA - Math operations", -> 60 | aether = new Aether language: "java" 61 | code = """ 62 | public class MathClass 63 | { 64 | public static String output() 65 | { 66 | int i1 = 10; 67 | int i2 = 2; 68 | int i4, i5, i6, i7, i8; 69 | i4 = i1 + i2; 70 | i5 = i1 - i2; 71 | i6 = i1 * i2; 72 | i7 = i1 / i2; 73 | i8 = i1 % i2; 74 | return i4+i5+i6-i7+i8; 75 | } 76 | } 77 | """ 78 | aether.className = "MathClass" 79 | aether.staticCall = "output" 80 | aether.transpile(code) 81 | expect(aether.run()).toEqual 35 82 | 83 | it "05 - JAVA - String concatenation", -> 84 | aether = new Aether language: "java" 85 | code = """ 86 | public class ConcatenationClass 87 | { 88 | public static String output() 89 | { 90 | String x = "String "; 91 | String y = "concatenation"; 92 | x = x + y; 93 | return x; 94 | } 95 | } 96 | """ 97 | aether.className = "ConcatenationClass" 98 | aether.staticCall = "output" 99 | aether.transpile(code) 100 | expect(aether.run()).toBe('String concatenation') 101 | 102 | it "05 - JAVA - If-else clause", -> 103 | aether = new Aether language: "java" 104 | code = """ 105 | public class IfClass 106 | { 107 | public static String output() 108 | { 109 | int a = 10; 110 | if (a == 10) 111 | { 112 | return "correct"; 113 | } 114 | else 115 | { 116 | return "incorrect"; 117 | } 118 | } 119 | } 120 | """ 121 | aether.className = "IfClass" 122 | aether.staticCall = "output" 123 | aether.transpile(code) 124 | expect(aether.run()).toBe('correct') 125 | 126 | it "05 - JAVA - For loop", -> 127 | aether = new Aether language: "java" 128 | code = """ 129 | public class ForClass 130 | { 131 | public static String output() 132 | { 133 | int x = 0; 134 | for (int i = 0 ; i < 10; i++ ){ 135 | x = x + i; 136 | } 137 | return x; 138 | } 139 | } 140 | """ 141 | aether.className = "ForClass" 142 | aether.staticCall = "output" 143 | aether.transpile(code) 144 | expect(aether.run()).toEqual 45 145 | 146 | it "05 - JAVA - While loop", -> 147 | aether = new Aether language: "java" 148 | code = """ 149 | public class WhileClass 150 | { 151 | public static String output() 152 | { 153 | int i = 0; 154 | while(i < 10){ 155 | i+= 1; 156 | } 157 | return i; 158 | } 159 | } 160 | """ 161 | aether.className = "WhileClass" 162 | aether.staticCall = "output" 163 | aether.transpile(code) 164 | expect(aether.run()).toEqual 10 165 | 166 | it "07 - JAVA - Two Dimensions array", -> 167 | aether = new Aether language: "java" 168 | code = """ 169 | public class ArrayClass 170 | { 171 | public static String output() 172 | { 173 | int[][] i = new int[3][2]; 174 | i[0][0] = 1; 175 | i[0][1] = 1; 176 | i[1][0] = 2; 177 | i[1][1] = 2; 178 | i[2][0] = 3; 179 | i[2][1] = 3; 180 | return i[2][1]; 181 | } 182 | } 183 | """ 184 | aether.className = "ArrayClass" 185 | aether.staticCall = "output" 186 | aether.transpile(code) 187 | expect(aether.run()).toEqual 3 188 | 189 | it "07 - JAVA - Ternary If", -> 190 | aether = new Aether language: "java" 191 | code = """ 192 | public class TernaryClass 193 | { 194 | public static String output() 195 | { 196 | int i = 100; 197 | return i >= 100 ? "Correct" : "Incorrect"; 198 | } 199 | } 200 | """ 201 | aether.className = "TernaryClass" 202 | aether.staticCall = "output" 203 | aether.transpile(code) 204 | expect(aether.run()).toBe('Correct') 205 | 206 | it "07 - JAVA - Switch", -> 207 | aether = new Aether language: "java" 208 | code = """ 209 | public class SwitchClass 210 | { 211 | public static String output() 212 | { 213 | int i = 10; 214 | switch(i) 215 | { 216 | case 0: return "That is zero"; 217 | case 1: return "That is one"; break; 218 | default: return "That is not zero nor one"; 219 | } 220 | } 221 | } 222 | """ 223 | aether.className = "SwitchClass" 224 | aether.staticCall = "output" 225 | aether.transpile(code) 226 | expect(aether.run()).toBe('That is not zero nor one') 227 | 228 | it "07 - JAVA - Increment and decrement outside For clause", -> 229 | aether = new Aether language: "java" 230 | code = """ 231 | public class IncrementClass 232 | { 233 | public static int output() 234 | { 235 | int i = 10; 236 | i++; 237 | i++; 238 | i--; 239 | return i; 240 | } 241 | } 242 | """ 243 | aether.className = "IncrementClass" 244 | aether.staticCall = "output" 245 | aether.transpile(code) 246 | expect(aether.run()).toEqual 11 247 | 248 | it "07 - JAVA - Assignment operators", -> 249 | aether = new Aether language: "java" 250 | code = """ 251 | public class AssignmentClass 252 | { 253 | public static int output() 254 | { 255 | double d1 = 5; 256 | double d2 = 2; 257 | d1 += d2; 258 | d1 -= d2; 259 | d1 *= d2; 260 | d1 /= d2; 261 | d1 %= d2; 262 | return d1; 263 | } 264 | } 265 | """ 266 | aether.className = "AssignmentClass" 267 | aether.staticCall = "output" 268 | aether.transpile(code) 269 | expect(aether.run()).toEqual 1 270 | 271 | it "07 - JAVA - Bitwise operators", -> 272 | aether = new Aether language: "java" 273 | code = """ 274 | public class BitwiseClass 275 | { 276 | public static int output() 277 | { 278 | int a = 60, b = 13; 279 | /*a = 0011 1100 280 | b = 0000 1100 281 | a&b = 0000 1100 (12) 282 | a|b = 0011 1101 (61) 283 | a^b = 0011 0001 (49) 284 | ~a = 1100 0011 (-61) 285 | a<<2 = 1111 0000 (240) 286 | a>>2 = 0000 1111 (15) 287 | a>>>2 = 0000 1111 (15)*/ 288 | int c = a&b; 289 | int d = a|b; 290 | int e = a^b; 291 | int f = ~a; 292 | int g = a<<2; 293 | int h = a >> 2; 294 | int i = a >>> 2; 295 | return c == 12 && d == 61 && e == 49 && f == -61 && g == 240 && h == 15 && i == 15; 296 | } 297 | } 298 | """ 299 | aether.className = "BitwiseClass" 300 | aether.staticCall = "output" 301 | aether.transpile(code) 302 | expect(aether.run()).toBe true 303 | 304 | it "07 - JAVA - If/else without bracers", -> 305 | aether = new Aether language: "java" 306 | code = """ 307 | public class IfClass 308 | { 309 | public static int output() 310 | { 311 | 312 | int a = 10; 313 | if (a == 10) 314 | return "that´s correct"; 315 | else 316 | return "that´s incorrect"; 317 | } 318 | } 319 | """ 320 | aether.className = "IfClass" 321 | aether.staticCall = "output" 322 | aether.transpile(code) 323 | expect(aether.run()).toEqual('that´s correct') 324 | 325 | it "07 - JAVA - Class method invocation", -> 326 | aether = new Aether language: "java" 327 | code = """ 328 | public class SumClass 329 | { 330 | public static int sum(int a, int b){ 331 | return a + b; 332 | } 333 | public static int output() 334 | { 335 | int i1 = 10; 336 | int i2 = 10; 337 | return sum(i1,i2); 338 | } 339 | } 340 | """ 341 | aether.className = "SumClass" 342 | aether.staticCall = "output" 343 | aether.transpile(code) 344 | expect(aether.run()).toEqual 20 345 | 346 | it "09 - JAVA - Instance variables from main class", -> 347 | aether = new Aether language: "java" 348 | code = """ 349 | public class VariableClass 350 | { 351 | private int a; 352 | 353 | public VariableClass(int a){ 354 | this.a = a; 355 | } 356 | 357 | public int getA(){ 358 | return this.a; 359 | } 360 | 361 | public static String output() 362 | { 363 | VariableClass vc = new VariableClass(10); 364 | if (vc.getA() == 10) 365 | return "that´s correct"; 366 | else 367 | return "that´s incorrect"; 368 | } 369 | } 370 | """ 371 | aether.className = "VariableClass" 372 | aether.staticCall = "output" 373 | aether.transpile(code) 374 | expect(aether.run()).toEqual('that´s correct') 375 | 376 | it "09 - Conditional yielding", -> 377 | aether = new Aether language: "java", yieldConditionally: true, simpleLoops: false 378 | dude = 379 | killCount: 0 380 | slay: -> 381 | @killCount += 1 382 | aether._shouldYield = true 383 | getKillCount: -> return @killCount 384 | code = """ 385 | public class YieldClass 386 | { 387 | public static String output() 388 | { 389 | while(true){ 390 | hero.slay(); 391 | break; 392 | } 393 | 394 | while(true){ 395 | hero.slay(); 396 | if (hero.getKillCount() >= 5) 397 | break; 398 | } 399 | hero.slay(); 400 | } 401 | } 402 | """ 403 | aether.className = "YieldClass" 404 | aether.staticCall = "output" 405 | aether.transpile code 406 | f = aether.createFunction() 407 | gen = f.apply dude 408 | 409 | for i in [1..6] 410 | expect(gen.next().done).toEqual false 411 | expect(dude.killCount).toEqual i 412 | expect(gen.next().done).toEqual true 413 | expect(dude.killCount).toEqual 6 -------------------------------------------------------------------------------- /test/lint_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | describe "Linting Test Suite", -> 4 | describe "Default linting", -> 5 | aether = new Aether() 6 | it "Should warn about missing semicolons", -> 7 | code = "var bandersnatch = 'frumious'" 8 | warnings = aether.lint(code).warnings 9 | expect(warnings.length).toEqual 1 10 | expect(warnings[0].message).toEqual 'Missing semicolon.' 11 | 12 | describe "Custom lint levels", -> 13 | it "Should allow ignoring of warnings", -> 14 | options = problems: {jshint_W033: {level: 'ignore'}} 15 | aether = new Aether options 16 | code = "var bandersnatch = 'frumious'" 17 | warnings = aether.lint(code).warnings 18 | expect(warnings.length).toEqual 0 19 | -------------------------------------------------------------------------------- /test/lua_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | lodash = require 'lodash' 3 | 4 | language = 'lua' 5 | aether = new Aether language: language 6 | 7 | luaEval = (code, that) -> 8 | aether.reset() 9 | aether.transpile(code) 10 | return aether.run() 11 | 12 | describe "#{language} Test suite", -> 13 | describe "Basics", -> 14 | it "return 1000", -> 15 | expect(luaEval(""" 16 | 17 | return 1000 18 | 19 | """)).toEqual 1000 20 | it "simple if", -> 21 | expect(luaEval(""" 22 | if false then return 2000 end 23 | return 1000 24 | """)).toBe(1000) 25 | 26 | it "multiple elif", -> 27 | expect(luaEval(""" 28 | local x = 4 29 | if x == 2 then 30 | x = x + 1 31 | return '2' 32 | elseif x == 44564 then 33 | x = x + 1 34 | return '44564' 35 | elseif x == 4 then 36 | x = x + 1 37 | return x 38 | end 39 | """)).toBe(5) 40 | 41 | it "mathmetics order", -> 42 | code = """ 43 | return (2*2 + 2/2 - 2*2/2) 44 | """ 45 | aether.transpile(code) 46 | expect(aether.run()).toBe(3) 47 | 48 | it "fibonacci function", -> 49 | expect(luaEval(""" 50 | function fib(n) 51 | if n < 2 then return n 52 | else return fib(n - 1) + fib(n - 2) end 53 | end 54 | chupacabra = fib(10) 55 | return chupacabra 56 | """)).toEqual 55 57 | 58 | it "for loop", -> 59 | expect(luaEval(""" 60 | data = {4, 2, 65, 7} 61 | total = 0 62 | for k,d in pairs(data) do 63 | total = total + d 64 | end 65 | return total 66 | """)).toBe(78) 67 | 68 | it "bubble sort", -> 69 | code = """ 70 | local function createShuffled(n) 71 | r = n * 10 + 1 72 | shuffle = {} 73 | for i=1,n do 74 | item = r * math.random() 75 | shuffle[#shuffle] = item 76 | end 77 | return shuffle 78 | end 79 | 80 | local function bubbleSort(data) 81 | sorted = false 82 | while not sorted do 83 | sorted = true 84 | for i=1,#data - 1 do 85 | if data[i] > data[i + 1] then 86 | t = data[i] 87 | data[i] = data[i + 1] 88 | data[i+1] = t 89 | sorted = false 90 | end 91 | end 92 | end 93 | return data 94 | end 95 | 96 | local function isSorted(data) 97 | for i=1,#data - 1 do 98 | if data[i] > data[i + 1] then 99 | return false 100 | end 101 | end 102 | return true 103 | end 104 | 105 | data = createShuffled(10) 106 | bubbleSort(data) 107 | return isSorted(data) 108 | """ 109 | aether.transpile(code) 110 | expect(aether.run()).toBe(true) 111 | 112 | it "dictionary", -> 113 | code = """ 114 | d = {p1='prop1'} 115 | return d['p1'] 116 | """ 117 | aether.transpile(code) 118 | expect(aether.run()).toBe('prop1') 119 | 120 | describe "Usage", -> 121 | it "self.doStuff via thisValue param", -> 122 | history = [] 123 | log = (s) -> 124 | expect(s).toEqual "hello" 125 | history.push s 126 | thisValue = {say: log} 127 | thisValue.moveDown = () -> 128 | expect(this).toEqual thisValue 129 | history.push 'moveDown' 130 | 131 | aetherOptions = { 132 | language: language 133 | } 134 | aether = new Aether aetherOptions 135 | code = """ 136 | self:moveDown() 137 | self:say('hello') 138 | """ 139 | aether.transpile code 140 | method = aether.createMethod thisValue 141 | aether.run method 142 | expect(history).toEqual(['moveDown', 'hello']) 143 | 144 | it "Math is fun?", -> 145 | thisValue = {} 146 | aetherOptions = { 147 | language: language 148 | } 149 | aether = new Aether aetherOptions 150 | code = """ 151 | return math.abs(-3) == 3 152 | """ 153 | aether.transpile code 154 | method = aether.createMethod thisValue 155 | expect(aether.run(method)).toEqual(true) 156 | 157 | it "self.getItems", -> 158 | history = [] 159 | getItems = () -> [{'pos':1}, {'pos':4}, {'pos':3}, {'pos':5}] 160 | move = (i) -> history.push i 161 | thisValue = {getItems: getItems, move: move} 162 | aetherOptions = { 163 | language: language 164 | } 165 | aether = new Aether aetherOptions 166 | code = """ 167 | local items = self.getItems() 168 | for k,item in pairs(items) do 169 | self.move(item['pos']) 170 | end 171 | """ 172 | aether.transpile code 173 | method = aether.createMethod thisValue 174 | aether.run method 175 | expect(history).toEqual([1, 4, 3, 5]) 176 | 177 | describe "Runtime problems", -> 178 | it "Should capture runtime problems", -> 179 | # 0123456789012345678901234567 180 | code = """ 181 | self:explode() 182 | self:exploooode() -- should error 183 | self:explode() 184 | """ 185 | explosions = [] 186 | thisValue = explode: -> explosions.push 'explosion!' 187 | aetherOptions = language: language 188 | aether = new Aether aetherOptions 189 | aether.transpile code 190 | method = aether.createMethod thisValue 191 | aether.run method 192 | expect(explosions).toEqual(['explosion!']) 193 | expect(aether.problems.errors.length).toEqual 1 194 | problem = aether.problems.errors[0] 195 | expect(problem.type).toEqual 'runtime' 196 | expect(problem.level).toEqual 'error' 197 | expect(problem.message).toMatch /exploooode/ 198 | expect(problem.range?.length).toEqual 2 199 | [start, end] = problem.range 200 | expect(start.ofs).toEqual 15 201 | expect(start.row).toEqual 1 202 | expect(start.col).toEqual 0 203 | expect(end.ofs).toEqual 32 204 | expect(end.row).toEqual 1 205 | expect(end.col).toEqual 17 206 | expect(problem.message).toMatch /Line 2/ 207 | 208 | 209 | describe "Yielding", -> 210 | it "Conditional yielding returns are correct", -> 211 | aether = new Aether language: "lua", yieldConditionally: true 212 | result = null 213 | dude = 214 | killCount: 0 215 | say: (v) -> result = v 216 | slay: -> 217 | @killCount += 1 218 | aether._shouldYield = true 219 | getKillCount: -> return @killCount 220 | code = """ 221 | function add(a,b) return a + b end 222 | self:slay() 223 | self:slay() 224 | local tosay = add(2,3) 225 | self:say(tosay) 226 | self:slay() 227 | """ 228 | aether.transpile code 229 | f = aether.createFunction() 230 | gen = f.apply dude 231 | 232 | for i in [1..3] 233 | expect(gen.next().done).toEqual false 234 | expect(dude.killCount).toEqual i 235 | expect(gen.next().done).toEqual true 236 | expect(result).toEqual 5 237 | 238 | it "Likes Simple Loops", -> 239 | aether = new Aether language: "lua", yieldConditionally: true, simpleLoops: true 240 | result = null 241 | dude = 242 | x: 0 243 | code = """ 244 | while true do 245 | self.x = self.x + 1 246 | end 247 | """ 248 | aether.transpile code 249 | f = aether.createFunction() 250 | gen = f.apply dude 251 | 252 | for i in [1..3] 253 | expect(gen.next().done).toEqual false 254 | expect(dude.x).toEqual i 255 | 256 | expect(gen.next().done).toEqual false 257 | -------------------------------------------------------------------------------- /test/statement_count_spec.coffee: -------------------------------------------------------------------------------- 1 | Aether = require '../aether' 2 | 3 | cs = (desc, lang, count, code) -> 4 | it "#{lang}: #{desc}", -> 5 | aether = new Aether 6 | language: lang 7 | aether.transpile code 8 | expect(aether.getStatementCount()).toEqual count 9 | 10 | describe "Statement Counting", -> 11 | describe "Python", -> 12 | cs "Simple", "python", 3, """ 13 | one() 14 | two() 15 | three() 16 | """ 17 | cs "Mathy", "python", 2, """ 18 | if self.a() > something.b and self.c(): 19 | x = somethingElse() 20 | """ 21 | cs "while loop", "python", 3, """ 22 | while True: 23 | self.moveLeft(); 24 | self.moveRight(); 25 | """ 26 | cs "for sum", "python", 3, """ 27 | for i in xrange(1,10): 28 | self.say(i) 29 | """ 30 | cs "function", "python", 3, """ 31 | def x(a): 32 | return a+2 33 | 34 | x(2) 35 | """ 36 | 37 | describe "Javascript", -> 38 | cs "Simple", "javascript", 3, """ 39 | one(); 40 | two(); 41 | three(); 42 | """ 43 | cs "Mathy", "javascript", 2, """ 44 | if ( this.a() > something.b && this.c() ) 45 | var x = somethingElse(); 46 | """ 47 | cs "while loop", "javascript", 3, """ 48 | while ( true ) { 49 | self.moveLeft(); 50 | self.moveRight(); 51 | } 52 | """ 53 | cs "for sum", "javascript", 3, """ 54 | for ( var i = 0; i < 10; ++i ) this.say(i); 55 | """ 56 | cs "function", "javascript", 3, """ 57 | function x(a) { 58 | return x + 2; 59 | } 60 | x(2); 61 | """ 62 | 63 | describe "Lua", -> 64 | cs "Simple", "lua", 3, """ 65 | one() 66 | two() 67 | three() 68 | """ 69 | cs "Mathy", "lua", 2, """ 70 | if (self:a() > something.b) and self.c() then 71 | x = somethingElse() 72 | end 73 | """ 74 | cs "for sum", "lua", 2, """ 75 | for i = 1,10 do this.say(i) end 76 | """ 77 | 78 | 79 | --------------------------------------------------------------------------------