├── .arc └── npm-test │ ├── __phutil_library_init__.php │ ├── __phutil_library_map__.php │ └── src │ └── NpmUnitTestEngine.php ├── .arcconfig ├── .clang-format ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── lib ├── base.ts ├── call.ts ├── declaration.ts ├── expression.ts ├── facade_converter.ts ├── literal.ts ├── main.ts ├── mkdirp.ts ├── module.ts ├── statement.ts └── type.ts ├── package.json ├── test ├── call_test.ts ├── declaration_test.ts ├── decorator_test.ts ├── e2e │ ├── angular2 │ │ └── src │ │ │ └── facade │ │ │ └── lang.d.ts │ ├── helloworld.ts │ ├── lib.ts │ ├── pubspec.yaml │ ├── test.d.ts │ └── tsconfig.json ├── expression_test.ts ├── facade_converter_test.ts ├── function_test.ts ├── literal_test.ts ├── main_test.ts ├── module_test.ts ├── statement_test.ts ├── test_support.ts ├── tsc_e2e │ ├── map_target │ │ └── dep.ts │ ├── p1 │ │ └── user.ts │ └── tsconfig.json └── type_test.ts ├── tsconfig.json ├── tsd.json └── tslint.json /.arc/npm-test/__phutil_library_init__.php: -------------------------------------------------------------------------------- 1 | 2, 11 | 'class' => array( 12 | 'NpmUnitTestEngine' => 'src/NpmUnitTestEngine.php', 13 | ), 14 | 'function' => array(), 15 | 'xmap' => array( 16 | 'NpmUnitTestEngine' => 'ArcanistUnitTestEngine', 17 | ), 18 | )); 19 | -------------------------------------------------------------------------------- /.arc/npm-test/src/NpmUnitTestEngine.php: -------------------------------------------------------------------------------- 1 | setName('npm test'); 8 | if ($retval == 0) { 9 | $result->setResult(ArcanistUnitTestResult::RESULT_PASS); 10 | } else { 11 | $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); 12 | } 13 | return array($result); 14 | } 15 | 16 | public function shouldEchoTestResults() { 17 | return false; 18 | } 19 | } 20 | ?> 21 | -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "base": "git:origin/master", 3 | "phabricator.uri" : "https://reviews.angular.io/", 4 | "load": [ 5 | ".arc/npm-test" 6 | ], 7 | "unit.engine": "NpmUnitTestEngine" 8 | } 9 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JS 2 | build 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | .phutil_module_cache 13 | 14 | # Type definitions installed with tsd 15 | typings 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | # Users Environment Variables 33 | .lock-wscript 34 | 35 | # IDEs 36 | .idea/ 37 | /ts2dart.iml 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | typings 2 | .idea 3 | .clang-format 4 | .travis.yml 5 | tsd.json 6 | build/test 7 | build/e2e 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart # NodeJS is available everywhere 2 | sudo: false 3 | node_js: 4 | - "stable" 5 | before_script: 6 | - npm install 7 | script: 8 | - npm test 9 | cache: 10 | directories: 11 | - node_modules 12 | - typings 13 | env: 14 | global: 15 | # Token for tsd to increase github rate limit 16 | # See https://github.com/DefinitelyTyped/tsd#tsdrc 17 | # This does not use http://docs.travis-ci.com/user/environment-variables/#Secure-Variables 18 | # because those are not visible for pull requests, and those should also be reliable. 19 | # This SSO token belongs to github account angular-github-ratelimit-token which has no access 20 | # (password is in Valentine) 21 | - TSD_GITHUB_TOKEN=ef474500309daea53d5991b3079159a29520a40b 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript to Dart transpiler [![Build Status](https://travis-ci.org/angular/ts2dart.svg?branch=master)](https://travis-ci.org/angular/ts2dart) 2 | 3 | ts2dart is a TypeScript to Dart transpiler. It's mainly used to translate Angular 2 from TypeScript 4 | to Dart for its Dart user base. 5 | 6 | ## Usage 7 | 8 | - To install as Command Line Tool execute: `npm i -g ts2dart` 9 | - Once installed you could run it doing: `ts2dart inputFile.ts` 10 | 11 | ## Installation 12 | 13 | - execute `npm i` to install the dependencies, 14 | - the Dart SDK must be available to run end to end tests. 15 | 16 | ## Gulp tasks 17 | 18 | - `gulp watch` executes the unit tests in watch mode (use `gulp test.unit` for a single run), 19 | - `gulp test.e2e` executes the e2e tests, 20 | - `gulp test.check-format` checks the source code formatting using `clang-format`, 21 | - `gulp test` runs unit tests, e2e tests and checks the source code formatting. 22 | 23 | ## Phabricator Reviews 24 | 25 | You can send pull requests via Github, or by creating a Phabricator diff on 26 | https://reviews.angular.io. Both are fine, though Phabricator has a nicer code review UI. 27 | 28 | To create a Phabricator diff: 29 | 30 | - create an account on https://reviews.angular.io 31 | - install [Arcanist](https://secure.phabricator.com/book/phabricator/article/arcanist/) 32 | - run `arc diff` to upload a diff (= pull request). This will also run all tests. 33 | - get it reviewed by entering a "Reviewer", e.g. "mprobst", "alexeagle", "viks", ... 34 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('source-map-support').install(); 2 | 3 | var clangFormat = require('clang-format'); 4 | var formatter = require('gulp-clang-format'); 5 | var fs = require('fs'); 6 | var fsx = require('fs-extra'); 7 | var gulp = require('gulp'); 8 | var gutil = require('gulp-util'); 9 | var merge = require('merge2'); 10 | var mocha = require('gulp-mocha'); 11 | var path = require('path'); 12 | var sourcemaps = require('gulp-sourcemaps'); 13 | var spawn = require('child_process').spawn; 14 | var ts = require('gulp-typescript'); 15 | var typescript = require('typescript'); 16 | var style = require('dart-style'); 17 | var which = require('which'); 18 | var tslint = require('gulp-tslint'); 19 | 20 | gulp.task('test.check-format', function() { 21 | return gulp.src(['*.js', 'lib/**/*.ts', 'test/**/*.ts']) 22 | .pipe(formatter.checkFormat('file', clangFormat)) 23 | .on('warning', onError); 24 | }); 25 | 26 | gulp.task('test.check-lint', function() { 27 | return gulp.src(['lib/**/*.ts', 'test/**/*.ts']) 28 | .pipe(tslint()) 29 | .pipe(tslint.report('verbose')) 30 | .on('warning', onError); 31 | }); 32 | 33 | var hasError; 34 | var failOnError = true; 35 | 36 | var onError = function(err) { 37 | hasError = true; 38 | gutil.log(err.message); 39 | if (failOnError) { 40 | process.exit(1); 41 | } 42 | }; 43 | 44 | var tsProject = 45 | ts.createProject('tsconfig.json', {noEmit: false, declaration: true, typescript: typescript}); 46 | 47 | gulp.task('compile', function() { 48 | hasError = false; 49 | var tsResult = 50 | gulp.src(['lib/**/*.ts', 'typings/**/*.d.ts', 'node_modules/typescript/lib/typescript.d.ts']) 51 | .pipe(sourcemaps.init()) 52 | .pipe(ts(tsProject)) 53 | .on('error', onError); 54 | return merge([ 55 | tsResult.dts.pipe(gulp.dest('build/definitions')), 56 | // Write external sourcemap next to the js file 57 | tsResult.js.pipe(sourcemaps.write('.')).pipe(gulp.dest('build/lib')), 58 | tsResult.js.pipe(gulp.dest('build/lib')), 59 | ]); 60 | }); 61 | 62 | gulp.task('test.compile', ['compile'], function(done) { 63 | if (hasError) { 64 | done(); 65 | return; 66 | } 67 | return gulp 68 | .src( 69 | ['test/*.ts', 'typings/**/*.d.ts', 'node_modules/dart-style/dart-style.d.ts'], 70 | {base: '.'}) 71 | .pipe(sourcemaps.init()) 72 | .pipe(ts(tsProject)) 73 | .on('error', onError) 74 | .js.pipe(sourcemaps.write()) 75 | .pipe(gulp.dest('build/')); // '/test/' comes from base above. 76 | }); 77 | 78 | gulp.task('test.unit', ['test.compile'], function(done) { 79 | if (hasError) { 80 | done(); 81 | return; 82 | } 83 | return gulp.src('build/test/**/*.js').pipe(mocha({ 84 | timeout: 4000, // Needed by the type-based tests :-( 85 | })); 86 | }); 87 | 88 | // This test transpiles some unittests to dart and runs them in the Dart VM. 89 | gulp.task('test.e2e', ['test.compile'], function(done) { 90 | var testfile = 'helloworld'; 91 | 92 | // Removes backslashes from __dirname in Windows 93 | var dir = (__dirname.replace(/\\/g, '/') + '/build/e2e'); 94 | if (fs.existsSync(dir)) fsx.removeSync(dir); 95 | fs.mkdirSync(dir); 96 | fsx.copySync(__dirname + '/test/e2e/pubspec.yaml', dir + '/pubspec.yaml'); 97 | 98 | // run node with a shell so we can wildcard all the .ts files 99 | var cmd = 'node build/lib/main.js --translateBuiltins --tsconfig test/e2e/tsconfig.json ' + 100 | '--generateLibraryName=true ' + 101 | 'test/e2e/*.ts'; 102 | // Paths must be relative to our source root, so run with cwd == dir. 103 | spawn('sh', ['-c', cmd], {stdio: 'inherit'}).on('close', function(code, signal) { 104 | if (code > 0) { 105 | onError(new Error('Failed to transpile ' + testfile + '.ts')); 106 | } else { 107 | try { 108 | var opts = {stdio: 'inherit', cwd: dir}; 109 | // Install the unittest packages on every run, using the content of pubspec.yaml 110 | // TODO: maybe this could be memoized or served locally? 111 | spawn(which.sync('pub'), ['install'], opts).on('close', function() { 112 | // Run the tests using built-in test runner. 113 | spawn(which.sync('dart'), [testfile + '.dart'], opts).on('close', done); 114 | }); 115 | } catch (e) { 116 | console.log('Dart SDK is not found on the PATH:', e.message); 117 | throw e; 118 | } 119 | } 120 | }); 121 | }); 122 | 123 | gulp.task('test.tsc_e2e', ['test.compile'], function(done) { 124 | // Test that "tsconfig.json" is read correctly. 125 | var outDir = (__dirname.replace(/\\/g, '/') + '/build/tsc_e2e'); 126 | if (fs.existsSync(outDir)) fsx.removeSync(outDir); 127 | fs.mkdirSync(outDir); 128 | 129 | var cmd = 'node build/lib/main.js --translateBuiltins --tsconfig test/tsc_e2e/tsconfig.json ' + 130 | '--generateLibraryName=true ' + 131 | 'test/tsc_e2e/p1/user.ts'; 132 | spawn('sh', ['-c', cmd], {stdio: 'inherit'}).on('close', function(code, signal) { 133 | if (code > 0) { 134 | onError(new Error('Failed to transpile ' + testfile + '.ts')); 135 | return; 136 | } 137 | var content = fs.readFileSync(path.join(outDir, 'p1/user.dart'), 'utf-8'); 138 | if (!content.match(/library p1\.user/) || !content.match(/import "package:mapped\/dep.dart"/) || 139 | !content.match(/Future/)) { 140 | throw new Error('incorrect content in p1.dart:\n' + content) 141 | } 142 | }); 143 | }); 144 | 145 | gulp.task( 146 | 'test', ['test.check-format', 'test.check-lint', 'test.unit', 'test.e2e', 'test.tsc_e2e']); 147 | 148 | gulp.task('watch', ['test.unit'], function() { 149 | failOnError = false; 150 | // Avoid watching generated .d.ts in the build (aka output) directory. 151 | return gulp.watch(['lib/**/*.ts', 'test/**/*.ts'], {ignoreInitial: true}, ['test.unit']); 152 | }); 153 | 154 | gulp.task('default', ['compile']); -------------------------------------------------------------------------------- /lib/base.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {Transpiler} from './main'; 3 | 4 | export type ClassLike = ts.ClassDeclaration | ts.InterfaceDeclaration; 5 | 6 | export function ident(n: ts.Node): string { 7 | if (n.kind === ts.SyntaxKind.Identifier) return (n).text; 8 | if (n.kind === ts.SyntaxKind.QualifiedName) { 9 | let qname = (n); 10 | let leftName = ident(qname.left); 11 | if (leftName) return leftName + '.' + ident(qname.right); 12 | } 13 | return null; 14 | } 15 | 16 | export class TranspilerBase { 17 | private idCounter: number = 0; 18 | constructor(private transpiler: Transpiler) {} 19 | 20 | visit(n: ts.Node) { this.transpiler.visit(n); } 21 | emit(s: string) { this.transpiler.emit(s); } 22 | emitNoSpace(s: string) { this.transpiler.emitNoSpace(s); } 23 | reportError(n: ts.Node, message: string) { this.transpiler.reportError(n, message); } 24 | 25 | visitNode(n: ts.Node): boolean { throw new Error('not implemented'); } 26 | 27 | visitEach(nodes: ts.Node[]) { nodes.forEach((n) => this.visit(n)); } 28 | 29 | visitEachIfPresent(nodes?: ts.Node[]) { 30 | if (nodes) this.visitEach(nodes); 31 | } 32 | 33 | visitList(nodes: ts.Node[], separator = ',') { 34 | for (let i = 0; i < nodes.length; i++) { 35 | this.visit(nodes[i]); 36 | if (i < nodes.length - 1) this.emit(separator); 37 | } 38 | } 39 | 40 | uniqueId(name: string): string { 41 | const id = this.idCounter++; 42 | return `_${name}\$\$ts2dart\$${id}`; 43 | } 44 | 45 | assert(c: ts.Node, condition: boolean, reason: string): void { 46 | if (!condition) { 47 | this.reportError(c, reason); 48 | throw new Error(reason); 49 | } 50 | } 51 | 52 | getAncestor(n: ts.Node, kind: ts.SyntaxKind): ts.Node { 53 | for (let parent = n; parent; parent = parent.parent) { 54 | if (parent.kind === kind) return parent; 55 | } 56 | return null; 57 | } 58 | 59 | hasAncestor(n: ts.Node, kind: ts.SyntaxKind): boolean { return !!this.getAncestor(n, kind); } 60 | 61 | hasAnnotation(decorators: ts.NodeArray, name: string): boolean { 62 | if (!decorators) return false; 63 | return decorators.some((d) => { 64 | let decName = ident(d.expression); 65 | if (decName === name) return true; 66 | if (d.expression.kind !== ts.SyntaxKind.CallExpression) return false; 67 | let callExpr = (d.expression); 68 | decName = ident(callExpr.expression); 69 | return decName === name; 70 | }); 71 | } 72 | 73 | hasFlag(n: {flags: number}, flag: ts.NodeFlags): boolean { 74 | return n && (n.flags & flag) !== 0 || false; 75 | } 76 | 77 | maybeDestructureIndexType(node: ts.TypeLiteralNode): [ts.TypeNode, ts.TypeNode] { 78 | let members = node.members; 79 | if (members.length !== 1 || members[0].kind !== ts.SyntaxKind.IndexSignature) { 80 | return null; 81 | } 82 | let indexSig = (members[0]); 83 | if (indexSig.parameters.length > 1) { 84 | this.reportError(indexSig, 'Expected an index signature to have a single parameter'); 85 | } 86 | return [indexSig.parameters[0].type, indexSig.type]; 87 | } 88 | 89 | 90 | getRelativeFileName(fileName: string): string { 91 | return this.transpiler.getRelativeFileName(fileName); 92 | } 93 | 94 | maybeVisitTypeArguments(n: {typeArguments?: ts.NodeArray}) { 95 | if (n.typeArguments) { 96 | // If it's a single type argument ``, ignore it and emit nothing. 97 | // This is particularly useful for `Promise`, see 98 | // https://github.com/dart-lang/sdk/issues/2231#issuecomment-108313639 99 | if (n.typeArguments.length === 1 && n.typeArguments[0].kind === ts.SyntaxKind.VoidKeyword) { 100 | return; 101 | } 102 | this.emitNoSpace('<'); 103 | this.visitList(n.typeArguments); 104 | this.emit('>'); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/call.ts: -------------------------------------------------------------------------------- 1 | import ts = require('typescript'); 2 | import base = require('./base'); 3 | import ts2dart = require('./main'); 4 | import {FacadeConverter} from './facade_converter'; 5 | 6 | export default class CallTranspiler extends base.TranspilerBase { 7 | constructor(tr: ts2dart.Transpiler, private fc: FacadeConverter) { super(tr); } 8 | 9 | visitNode(node: ts.Node): boolean { 10 | switch (node.kind) { 11 | case ts.SyntaxKind.Block: 12 | // This is a bit ugly: to separate Declarations from Calls, this code has to special case 13 | // blocks that are actually constructor bodies. 14 | if (node.parent && node.parent.kind === ts.SyntaxKind.Constructor) { 15 | return this.visitConstructorBody(node.parent); 16 | } 17 | return false; 18 | case ts.SyntaxKind.NewExpression: 19 | let newExpr = node; 20 | if (this.hasAncestor(node, ts.SyntaxKind.Decorator)) { 21 | // Constructor calls in annotations must be const constructor calls. 22 | this.emit('const'); 23 | } else if (this.fc.isInsideConstExpr(node)) { 24 | this.emit('const'); 25 | } else { 26 | // Some implementations can replace the `new` keyword. 27 | if (this.fc.shouldEmitNew(newExpr)) { 28 | this.emit('new'); 29 | } 30 | } 31 | if (this.fc.maybeHandleCall(newExpr)) break; 32 | this.visitCall(newExpr); 33 | break; 34 | case ts.SyntaxKind.CallExpression: 35 | let callExpr = node; 36 | if (this.fc.maybeHandleCall(callExpr)) break; 37 | if (this.maybeHandleSuperCall(callExpr)) break; 38 | this.visitCall(callExpr); 39 | break; 40 | case ts.SyntaxKind.SuperKeyword: 41 | this.emit('super'); 42 | break; 43 | default: 44 | return false; 45 | } 46 | return true; 47 | } 48 | 49 | private visitCall(c: ts.CallExpression) { 50 | if (c.expression.kind === ts.SyntaxKind.Identifier) { 51 | this.fc.visitTypeName(c.expression); 52 | } else { 53 | this.visit(c.expression); 54 | } 55 | if (c.typeArguments) { 56 | // For DDC, emit generic method arguments in /* block comments */ 57 | // NB: Surprisingly, whitespace within the comment is significant here :-( 58 | // TODO(martinprobst): Remove once Dart natively supports generic methods. 59 | if (c.kind !== ts.SyntaxKind.NewExpression) this.emit('/*'); 60 | this.maybeVisitTypeArguments(c); 61 | if (c.kind !== ts.SyntaxKind.NewExpression) this.emitNoSpace('*/'); 62 | } 63 | this.emit('('); 64 | if (c.arguments && !this.handleNamedParamsCall(c)) { 65 | this.visitList(c.arguments); 66 | } 67 | this.emit(')'); 68 | } 69 | 70 | private handleNamedParamsCall(c: ts.CallExpression): boolean { 71 | // Preamble: This is all committed in the name of backwards compat with the traceur transpiler. 72 | 73 | // Terrible hack: transform foo(a, b, {c: d}) into foo(a, b, c: d), which is Dart's calling 74 | // syntax for named/optional parameters. An alternative would be to transform the method 75 | // declaration to take a plain object literal and destructure in the method, but then client 76 | // code written against Dart wouldn't get nice named parameters. 77 | if (c.arguments.length === 0) return false; 78 | let last = c.arguments[c.arguments.length - 1]; 79 | if (last.kind !== ts.SyntaxKind.ObjectLiteralExpression) return false; 80 | let objLit = last; 81 | if (objLit.properties.length === 0) return false; 82 | // Even worse: foo(a, b, {'c': d}) is considered to *not* be a named parameters call. 83 | let hasNonPropAssignments = objLit.properties.some( 84 | (p) => 85 | (p.kind !== ts.SyntaxKind.PropertyAssignment || 86 | (p).name.kind !== ts.SyntaxKind.Identifier)); 87 | if (hasNonPropAssignments) return false; 88 | 89 | let len = c.arguments.length - 1; 90 | this.visitList(c.arguments.slice(0, len)); 91 | if (len) this.emit(','); 92 | let props = objLit.properties; 93 | for (let i = 0; i < props.length; i++) { 94 | let prop = props[i]; 95 | this.emit(base.ident(prop.name)); 96 | this.emit(':'); 97 | this.visit(prop.initializer); 98 | if (i < objLit.properties.length - 1) this.emit(','); 99 | } 100 | return true; 101 | } 102 | 103 | /** 104 | * Handles constructor initializer lists and bodies. 105 | * 106 | *

Dart's super() ctor calls have to be moved to the constructors initializer list, and `const` 107 | * constructors must be completely empty, only assigning into fields through the initializer list. 108 | * The code below finds super() calls and handles const constructors, marked with the special 109 | * `@CONST` annotation on the class. 110 | * 111 | *

Not emitting super() calls when traversing the ctor body is handled by maybeHandleSuperCall 112 | * below. 113 | */ 114 | private visitConstructorBody(ctor: ts.ConstructorDeclaration): boolean { 115 | let body = ctor.body; 116 | if (!body) return false; 117 | 118 | let errorAssignmentsSuper = 'const constructors can only contain assignments and super calls'; 119 | let errorThisAssignment = 'assignments in const constructors must assign into this.'; 120 | 121 | let parent = ctor.parent; 122 | let parentIsConst = this.fc.isConstClass(parent); 123 | let superCall: ts.CallExpression; 124 | let expressions: ts.Expression[] = []; 125 | // Find super() calls and (if in a const ctor) collect assignment expressions (not statements!) 126 | body.statements.forEach((stmt) => { 127 | if (stmt.kind !== ts.SyntaxKind.ExpressionStatement) { 128 | if (parentIsConst) this.reportError(stmt, errorAssignmentsSuper); 129 | return; 130 | } 131 | let nestedExpr = (stmt).expression; 132 | 133 | // super() call? 134 | if (nestedExpr.kind === ts.SyntaxKind.CallExpression) { 135 | let callExpr = nestedExpr; 136 | if (callExpr.expression.kind !== ts.SyntaxKind.SuperKeyword) { 137 | if (parentIsConst) this.reportError(stmt, errorAssignmentsSuper); 138 | return; 139 | } 140 | superCall = callExpr; 141 | return; 142 | } 143 | 144 | // this.x assignment? 145 | if (parentIsConst) { 146 | // Check for assignment. 147 | if (nestedExpr.kind !== ts.SyntaxKind.BinaryExpression) { 148 | this.reportError(nestedExpr, errorAssignmentsSuper); 149 | return; 150 | } 151 | let binExpr = nestedExpr; 152 | if (binExpr.operatorToken.kind !== ts.SyntaxKind.EqualsToken) { 153 | this.reportError(binExpr, errorAssignmentsSuper); 154 | return; 155 | } 156 | // Check for 'this.' 157 | if (binExpr.left.kind !== ts.SyntaxKind.PropertyAccessExpression) { 158 | this.reportError(binExpr, errorThisAssignment); 159 | return; 160 | } 161 | let lhs = binExpr.left; 162 | if (lhs.expression.kind !== ts.SyntaxKind.ThisKeyword) { 163 | this.reportError(binExpr, errorThisAssignment); 164 | return; 165 | } 166 | let ident = lhs.name; 167 | binExpr.left = ident; 168 | expressions.push(nestedExpr); 169 | } 170 | }); 171 | 172 | let hasInitializerExpr = expressions.length > 0; 173 | if (hasInitializerExpr) { 174 | // Write out the assignments. 175 | this.emit(':'); 176 | this.visitList(expressions); 177 | } 178 | if (superCall) { 179 | this.emit(hasInitializerExpr ? ',' : ':'); 180 | this.emit('super ('); 181 | if (!this.handleNamedParamsCall(superCall)) { 182 | this.visitList(superCall.arguments); 183 | } 184 | this.emit(')'); 185 | } 186 | if (parentIsConst) { 187 | // Const ctors don't have bodies. 188 | this.emit(';'); 189 | return true; // completely handled. 190 | } else { 191 | return false; 192 | } 193 | } 194 | 195 | /** 196 | * Checks whether `callExpr` is a super() call that should be ignored because it was already 197 | * handled by `maybeEmitSuperInitializer` above. 198 | */ 199 | private maybeHandleSuperCall(callExpr: ts.CallExpression): boolean { 200 | if (callExpr.expression.kind !== ts.SyntaxKind.SuperKeyword) return false; 201 | // Sanity check that there was indeed a ctor directly above this call. 202 | let exprStmt = callExpr.parent; 203 | let ctorBody = exprStmt.parent; 204 | let ctor = ctorBody.parent; 205 | if (ctor.kind !== ts.SyntaxKind.Constructor) { 206 | this.reportError(callExpr, 'super calls must be immediate children of their constructors'); 207 | return false; 208 | } 209 | this.emit('/* super call moved to initializer */'); 210 | return true; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /lib/declaration.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as base from './base'; 4 | import {FacadeConverter} from './facade_converter'; 5 | import {Transpiler} from './main'; 6 | 7 | export default class DeclarationTranspiler extends base.TranspilerBase { 8 | constructor( 9 | tr: Transpiler, private fc: FacadeConverter, private enforceUnderscoreConventions: boolean) { 10 | super(tr); 11 | } 12 | 13 | visitNode(node: ts.Node): boolean { 14 | switch (node.kind) { 15 | case ts.SyntaxKind.VariableDeclarationList: 16 | // Note: VariableDeclarationList can only occur as part of a for loop. 17 | let varDeclList = node; 18 | this.visitList(varDeclList.declarations); 19 | break; 20 | case ts.SyntaxKind.VariableDeclaration: 21 | let varDecl = node; 22 | this.visitVariableDeclarationType(varDecl); 23 | this.visit(varDecl.name); 24 | if (varDecl.initializer) { 25 | this.emit('='); 26 | this.visit(varDecl.initializer); 27 | } 28 | break; 29 | 30 | case ts.SyntaxKind.ClassDeclaration: 31 | let classDecl = node; 32 | if (classDecl.modifiers && (classDecl.modifiers.flags & ts.NodeFlags.Abstract)) { 33 | this.visitClassLike('abstract class', classDecl); 34 | } else { 35 | this.visitClassLike('class', classDecl); 36 | } 37 | break; 38 | case ts.SyntaxKind.InterfaceDeclaration: 39 | let ifDecl = node; 40 | // Function type interface in an interface with a single declaration 41 | // of a call signature (http://goo.gl/ROC5jN). 42 | if (ifDecl.members.length === 1 && ifDecl.members[0].kind === ts.SyntaxKind.CallSignature) { 43 | let member = ifDecl.members[0]; 44 | this.visitFunctionTypedefInterface(ifDecl.name.text, member, ifDecl.typeParameters); 45 | } else { 46 | this.visitClassLike('abstract class', ifDecl); 47 | } 48 | break; 49 | case ts.SyntaxKind.HeritageClause: 50 | let heritageClause = node; 51 | if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword && 52 | heritageClause.parent.kind !== ts.SyntaxKind.InterfaceDeclaration) { 53 | this.emit('extends'); 54 | } else { 55 | this.emit('implements'); 56 | } 57 | // Can only have one member for extends clauses. 58 | this.visitList(heritageClause.types); 59 | break; 60 | case ts.SyntaxKind.ExpressionWithTypeArguments: 61 | let exprWithTypeArgs = node; 62 | this.visit(exprWithTypeArgs.expression); 63 | this.maybeVisitTypeArguments(exprWithTypeArgs); 64 | break; 65 | case ts.SyntaxKind.EnumDeclaration: 66 | let decl = node; 67 | // The only legal modifier for an enum decl is const. 68 | let isConst = decl.modifiers && (decl.modifiers.flags & ts.NodeFlags.Const); 69 | if (isConst) { 70 | this.reportError(node, 'const enums are not supported'); 71 | } 72 | this.emit('enum'); 73 | this.fc.visitTypeName(decl.name); 74 | this.emit('{'); 75 | // Enums can be empty in TS ... 76 | if (decl.members.length === 0) { 77 | // ... but not in Dart. 78 | this.reportError(node, 'empty enums are not supported'); 79 | } 80 | this.visitList(decl.members); 81 | this.emit('}'); 82 | break; 83 | case ts.SyntaxKind.EnumMember: 84 | let member = node; 85 | this.visit(member.name); 86 | if (member.initializer) { 87 | this.reportError(node, 'enum initializers are not supported'); 88 | } 89 | break; 90 | case ts.SyntaxKind.Constructor: 91 | let ctorDecl = node; 92 | // Find containing class name. 93 | let className: ts.Identifier; 94 | for (let parent = ctorDecl.parent; parent; parent = parent.parent) { 95 | if (parent.kind === ts.SyntaxKind.ClassDeclaration) { 96 | className = (parent).name; 97 | break; 98 | } 99 | } 100 | if (!className) this.reportError(ctorDecl, 'cannot find outer class node'); 101 | this.visitDeclarationMetadata(ctorDecl); 102 | if (this.fc.isConstClass(ctorDecl.parent)) { 103 | this.emit('const'); 104 | } 105 | this.visit(className); 106 | this.visitParameters(ctorDecl.parameters); 107 | this.visit(ctorDecl.body); 108 | break; 109 | case ts.SyntaxKind.PropertyDeclaration: 110 | this.visitProperty(node); 111 | break; 112 | case ts.SyntaxKind.SemicolonClassElement: 113 | // No-op, don't emit useless declarations. 114 | break; 115 | case ts.SyntaxKind.MethodDeclaration: 116 | this.visitDeclarationMetadata(node); 117 | this.visitFunctionLike(node); 118 | break; 119 | case ts.SyntaxKind.GetAccessor: 120 | this.visitDeclarationMetadata(node); 121 | this.visitFunctionLike(node, 'get'); 122 | break; 123 | case ts.SyntaxKind.SetAccessor: 124 | this.visitDeclarationMetadata(node); 125 | this.visitFunctionLike(node, 'set'); 126 | break; 127 | case ts.SyntaxKind.FunctionDeclaration: 128 | let funcDecl = node; 129 | this.visitDecorators(funcDecl.decorators); 130 | this.visitFunctionLike(funcDecl); 131 | break; 132 | case ts.SyntaxKind.ArrowFunction: 133 | let arrowFunc = node; 134 | // Dart only allows expressions following the fat arrow operator. 135 | // If the body is a block, we have to drop the fat arrow and emit an 136 | // anonymous function instead. 137 | if (arrowFunc.body.kind === ts.SyntaxKind.Block) { 138 | this.visitFunctionLike(arrowFunc); 139 | } else { 140 | this.visitParameters(arrowFunc.parameters); 141 | this.emit('=>'); 142 | this.visit(arrowFunc.body); 143 | } 144 | break; 145 | case ts.SyntaxKind.FunctionExpression: 146 | let funcExpr = node; 147 | this.visitFunctionLike(funcExpr); 148 | break; 149 | case ts.SyntaxKind.PropertySignature: 150 | let propSig = node; 151 | this.visitProperty(propSig); 152 | break; 153 | case ts.SyntaxKind.MethodSignature: 154 | let methodSignatureDecl = node; 155 | this.visitEachIfPresent(methodSignatureDecl.modifiers); 156 | this.visitFunctionLike(methodSignatureDecl); 157 | break; 158 | case ts.SyntaxKind.Parameter: 159 | let paramDecl = node; 160 | // Property parameters will have an explicit property declaration, so we just 161 | // need the dart assignment shorthand to reference the property. 162 | if (this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Public) || 163 | this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Private) || 164 | this.hasFlag(paramDecl.modifiers, ts.NodeFlags.Protected)) { 165 | this.visitDeclarationMetadata(paramDecl); 166 | this.emit('this .'); 167 | this.visit(paramDecl.name); 168 | if (paramDecl.initializer) { 169 | this.emit('='); 170 | this.visit(paramDecl.initializer); 171 | } 172 | break; 173 | } 174 | if (paramDecl.dotDotDotToken) this.reportError(node, 'rest parameters are unsupported'); 175 | if (paramDecl.name.kind === ts.SyntaxKind.ObjectBindingPattern) { 176 | this.visitNamedParameter(paramDecl); 177 | break; 178 | } 179 | this.visitDecorators(paramDecl.decorators); 180 | 181 | if (paramDecl.type && paramDecl.type.kind === ts.SyntaxKind.FunctionType) { 182 | // Dart uses "returnType paramName ( parameters )" syntax. 183 | let fnType = paramDecl.type; 184 | let hasRestParameter = fnType.parameters.some(p => !!p.dotDotDotToken); 185 | if (hasRestParameter) { 186 | // Dart does not support rest parameters/varargs, degenerate to just "Function". 187 | this.emit('Function'); 188 | this.visit(paramDecl.name); 189 | } else { 190 | this.visit(fnType.type); 191 | this.visit(paramDecl.name); 192 | this.visitParameters(fnType.parameters); 193 | } 194 | } else { 195 | if (paramDecl.type) this.visit(paramDecl.type); 196 | this.visit(paramDecl.name); 197 | } 198 | if (paramDecl.initializer) { 199 | this.emit('='); 200 | this.visit(paramDecl.initializer); 201 | } 202 | break; 203 | case ts.SyntaxKind.StaticKeyword: 204 | this.emit('static'); 205 | break; 206 | case ts.SyntaxKind.AbstractKeyword: 207 | // Abstract methods in Dart simply lack implementation, 208 | // and don't use the 'abstract' modifier 209 | // Abstract classes are handled in `case ts.SyntaxKind.ClassDeclaration` above. 210 | break; 211 | case ts.SyntaxKind.PrivateKeyword: 212 | // no-op, handled through '_' naming convention in Dart. 213 | break; 214 | case ts.SyntaxKind.PublicKeyword: 215 | // Handled in `visitDeclarationMetadata` below. 216 | break; 217 | case ts.SyntaxKind.ProtectedKeyword: 218 | // Handled in `visitDeclarationMetadata` below. 219 | break; 220 | 221 | default: 222 | return false; 223 | } 224 | return true; 225 | } 226 | 227 | private visitVariableDeclarationType(varDecl: ts.VariableDeclaration) { 228 | /* Note: VariableDeclarationList can only occur as part of a for loop. This helper method 229 | * is meant for processing for-loop variable declaration types only. 230 | * 231 | * In Dart, all variables in a variable declaration list must have the same type. Since 232 | * we are doing syntax directed translation, we cannot reliably determine if distinct 233 | * variables are declared with the same type or not. Hence we support the following cases: 234 | * 235 | * - A variable declaration list with a single variable can be explicitly typed. 236 | * - When more than one variable is in the list, all must be implicitly typed. 237 | */ 238 | let firstDecl = varDecl.parent.declarations[0]; 239 | let msg = 'Variables in a declaration list of more than one variable cannot by typed'; 240 | let isFinal = this.hasFlag(varDecl.parent, ts.NodeFlags.Const); 241 | let isConst = false; 242 | if (isFinal && varDecl.initializer) { 243 | // "const" in TypeScript/ES6 corresponds to "final" in Dart, i.e. reference constness. 244 | // If a "const" variable is immediately initialized to a CONST_EXPR(), special case it to be 245 | // a deeply const constant, and generate "const ...". 246 | isConst = varDecl.initializer.kind === ts.SyntaxKind.StringLiteral || 247 | varDecl.initializer.kind === ts.SyntaxKind.NumericLiteral || 248 | this.fc.isConstExpr(varDecl.initializer); 249 | } 250 | if (firstDecl === varDecl) { 251 | if (isConst) { 252 | this.emit('const'); 253 | } else if (isFinal) { 254 | this.emit('final'); 255 | } 256 | if (!varDecl.type) { 257 | if (!isFinal) this.emit('var'); 258 | } else if (varDecl.parent.declarations.length > 1) { 259 | this.reportError(varDecl, msg); 260 | } else { 261 | this.visit(varDecl.type); 262 | } 263 | } else if (varDecl.type) { 264 | this.reportError(varDecl, msg); 265 | } 266 | } 267 | 268 | private visitFunctionLike(fn: ts.FunctionLikeDeclaration, accessor?: string) { 269 | this.fc.pushTypeParameterNames(fn); 270 | try { 271 | if (fn.type) { 272 | if (fn.kind === ts.SyntaxKind.ArrowFunction || 273 | fn.kind === ts.SyntaxKind.FunctionExpression) { 274 | // The return type is silently dropped for function expressions (including arrow 275 | // functions), it is not supported in Dart. 276 | this.emit('/*'); 277 | this.visit(fn.type); 278 | this.emit('*/'); 279 | } else { 280 | this.visit(fn.type); 281 | } 282 | } 283 | if (accessor) this.emit(accessor); 284 | if (fn.name) this.visit(fn.name); 285 | if (fn.typeParameters) { 286 | this.emit('/*<'); 287 | // Emit the names literally instead of visiting, otherwise they will be replaced with the 288 | // comment hack themselves. 289 | this.emit(fn.typeParameters.map(p => base.ident(p.name)).join(', ')); 290 | this.emit('>*/'); 291 | } 292 | // Dart does not even allow the parens of an empty param list on getter 293 | if (accessor !== 'get') { 294 | this.visitParameters(fn.parameters); 295 | } else { 296 | if (fn.parameters && fn.parameters.length > 0) { 297 | this.reportError(fn, 'getter should not accept parameters'); 298 | } 299 | } 300 | if (fn.body) { 301 | this.visit(fn.body); 302 | } else { 303 | this.emit(';'); 304 | } 305 | } finally { 306 | this.fc.popTypeParameterNames(fn); 307 | } 308 | } 309 | 310 | private visitParameters(parameters: ts.ParameterDeclaration[]) { 311 | this.emit('('); 312 | let firstInitParamIdx = 0; 313 | for (; firstInitParamIdx < parameters.length; firstInitParamIdx++) { 314 | // ObjectBindingPatterns are handled within the parameter visit. 315 | let isOpt = 316 | parameters[firstInitParamIdx].initializer || parameters[firstInitParamIdx].questionToken; 317 | if (isOpt && parameters[firstInitParamIdx].name.kind !== ts.SyntaxKind.ObjectBindingPattern) { 318 | break; 319 | } 320 | } 321 | 322 | if (firstInitParamIdx !== 0) { 323 | let requiredParams = parameters.slice(0, firstInitParamIdx); 324 | this.visitList(requiredParams); 325 | } 326 | 327 | if (firstInitParamIdx !== parameters.length) { 328 | if (firstInitParamIdx !== 0) this.emit(','); 329 | let positionalOptional = parameters.slice(firstInitParamIdx, parameters.length); 330 | this.emit('['); 331 | this.visitList(positionalOptional); 332 | this.emit(']'); 333 | } 334 | 335 | this.emit(')'); 336 | } 337 | 338 | /** 339 | * Visit a property declaration. 340 | * In the special case of property parameters in a constructor, we also allow a parameter to be 341 | * emitted as a property. 342 | */ 343 | private visitProperty(decl: ts.PropertyDeclaration|ts.ParameterDeclaration, isParameter = false) { 344 | if (!isParameter) this.visitDeclarationMetadata(decl); 345 | let containingClass = (isParameter ? decl.parent.parent : decl.parent); 346 | let isConstField = 347 | this.fc.hasConstComment(decl) || this.hasAnnotation(decl.decorators, 'CONST'); 348 | let hasConstCtor = this.fc.isConstClass(containingClass); 349 | if (isConstField) { 350 | // const implies final 351 | this.emit('const'); 352 | } else { 353 | if (hasConstCtor) { 354 | this.emit('final'); 355 | } 356 | } 357 | if (decl.type) { 358 | this.visit(decl.type); 359 | } else if (!isConstField && !hasConstCtor) { 360 | this.emit('var'); 361 | } 362 | this.visit(decl.name); 363 | if (decl.initializer && !isParameter) { 364 | this.emit('='); 365 | this.visit(decl.initializer); 366 | } 367 | this.emit(';'); 368 | } 369 | 370 | private visitClassLike(keyword: string, decl: base.ClassLike) { 371 | this.visitDecorators(decl.decorators); 372 | this.emit(keyword); 373 | this.fc.visitTypeName(decl.name); 374 | if (decl.typeParameters) { 375 | this.emit('<'); 376 | this.visitList(decl.typeParameters); 377 | this.emit('>'); 378 | } 379 | this.visitEachIfPresent(decl.heritageClauses); 380 | this.emit('{'); 381 | 382 | // Synthesize explicit properties for ctor with 'property parameters' 383 | let synthesizePropertyParam = (param: ts.ParameterDeclaration) => { 384 | if (this.hasFlag(param.modifiers, ts.NodeFlags.Public) || 385 | this.hasFlag(param.modifiers, ts.NodeFlags.Private) || 386 | this.hasFlag(param.modifiers, ts.NodeFlags.Protected)) { 387 | // TODO: we should enforce the underscore prefix on privates 388 | this.visitProperty(param, true); 389 | } 390 | }; 391 | (>decl.members) 392 | .filter((m) => m.kind === ts.SyntaxKind.Constructor) 393 | .forEach( 394 | (ctor) => 395 | (ctor).parameters.forEach(synthesizePropertyParam)); 396 | this.visitEachIfPresent(decl.members); 397 | 398 | // Generate a constructor to host the const modifier, if needed 399 | if (this.fc.isConstClass(decl) && 400 | !(>decl.members) 401 | .some((m) => m.kind === ts.SyntaxKind.Constructor)) { 402 | this.emit('const'); 403 | this.fc.visitTypeName(decl.name); 404 | this.emit('();'); 405 | } 406 | this.emit('}'); 407 | } 408 | 409 | private visitDecorators(decorators: ts.NodeArray) { 410 | if (!decorators) return; 411 | 412 | decorators.forEach((d) => { 413 | // Special case @CONST 414 | let name = base.ident(d.expression); 415 | if (!name && d.expression.kind === ts.SyntaxKind.CallExpression) { 416 | // Unwrap @CONST() 417 | let callExpr = (d.expression); 418 | name = base.ident(callExpr.expression); 419 | } 420 | // Make sure these match IGNORED_ANNOTATIONS below. 421 | if (name === 'CONST') { 422 | // Ignore @CONST - it is handled above in visitClassLike. 423 | return; 424 | } 425 | this.emit('@'); 426 | this.visit(d.expression); 427 | }); 428 | } 429 | 430 | private visitDeclarationMetadata(decl: ts.Declaration) { 431 | this.visitDecorators(decl.decorators); 432 | this.visitEachIfPresent(decl.modifiers); 433 | 434 | if (this.hasFlag(decl.modifiers, ts.NodeFlags.Protected)) { 435 | this.reportError(decl, 'protected declarations are unsupported'); 436 | return; 437 | } 438 | if (!this.enforceUnderscoreConventions) return; 439 | // Early return in case this is a decl with no name, such as a constructor 440 | if (!decl.name) return; 441 | let name = base.ident(decl.name); 442 | if (!name) return; 443 | let isPrivate = this.hasFlag(decl.modifiers, ts.NodeFlags.Private); 444 | let matchesPrivate = !!name.match(/^_/); 445 | if (isPrivate && !matchesPrivate) { 446 | this.reportError(decl, 'private members must be prefixed with "_"'); 447 | } 448 | if (!isPrivate && matchesPrivate) { 449 | this.reportError(decl, 'public members must not be prefixed with "_"'); 450 | } 451 | } 452 | 453 | private visitNamedParameter(paramDecl: ts.ParameterDeclaration) { 454 | this.visitDecorators(paramDecl.decorators); 455 | let bp = paramDecl.name; 456 | let propertyTypes = this.fc.resolvePropertyTypes(paramDecl.type); 457 | let initMap = this.getInitializers(paramDecl); 458 | this.emit('{'); 459 | for (let i = 0; i < bp.elements.length; i++) { 460 | let elem = bp.elements[i]; 461 | let propDecl = propertyTypes[base.ident(elem.name)]; 462 | if (propDecl && propDecl.type) this.visit(propDecl.type); 463 | this.visit(elem.name); 464 | if (elem.initializer && initMap[base.ident(elem.name)]) { 465 | this.reportError(elem, 'cannot have both an inner and outer initializer'); 466 | } 467 | let init = elem.initializer || initMap[base.ident(elem.name)]; 468 | if (init) { 469 | this.emit(':'); 470 | this.visit(init); 471 | } 472 | if (i + 1 < bp.elements.length) this.emit(','); 473 | } 474 | this.emit('}'); 475 | } 476 | 477 | private getInitializers(paramDecl: ts.ParameterDeclaration) { 478 | let res: ts.Map = {}; 479 | if (!paramDecl.initializer) return res; 480 | if (paramDecl.initializer.kind !== ts.SyntaxKind.ObjectLiteralExpression) { 481 | this.reportError(paramDecl, 'initializers for named parameters must be object literals'); 482 | return res; 483 | } 484 | for (let i of (paramDecl.initializer).properties) { 485 | if (i.kind !== ts.SyntaxKind.PropertyAssignment) { 486 | this.reportError(i, 'named parameter initializers must be properties, got ' + i.kind); 487 | continue; 488 | } 489 | let ole = i; 490 | res[base.ident(ole.name)] = ole.initializer; 491 | } 492 | return res; 493 | } 494 | 495 | /** 496 | * Handles a function typedef-like interface, i.e. an interface that only declares a single 497 | * call signature, by translating to a Dart `typedef`. 498 | */ 499 | private visitFunctionTypedefInterface( 500 | name: string, signature: ts.CallSignatureDeclaration, 501 | typeParameters: ts.NodeArray) { 502 | this.emit('typedef'); 503 | if (signature.type) { 504 | this.visit(signature.type); 505 | } 506 | this.emit(name); 507 | if (typeParameters) { 508 | this.emit('<'); 509 | this.visitList(typeParameters); 510 | this.emit('>'); 511 | } 512 | this.visitParameters(signature.parameters); 513 | this.emit(';'); 514 | } 515 | } 516 | -------------------------------------------------------------------------------- /lib/expression.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as base from './base'; 4 | import {FacadeConverter} from './facade_converter'; 5 | import {Transpiler} from './main'; 6 | 7 | export default class ExpressionTranspiler extends base.TranspilerBase { 8 | constructor(tr: Transpiler, private fc: FacadeConverter) { super(tr); } 9 | 10 | visitNode(node: ts.Node): boolean { 11 | switch (node.kind) { 12 | case ts.SyntaxKind.BinaryExpression: 13 | let binExpr = node; 14 | let operatorKind = binExpr.operatorToken.kind; 15 | let tokenStr = ts.tokenToString(operatorKind); 16 | switch (operatorKind) { 17 | case ts.SyntaxKind.EqualsEqualsEqualsToken: 18 | case ts.SyntaxKind.ExclamationEqualsEqualsToken: 19 | if (operatorKind === ts.SyntaxKind.ExclamationEqualsEqualsToken) this.emit('!'); 20 | this.emit('identical ('); 21 | this.visit(binExpr.left); 22 | this.emit(','); 23 | this.visit(binExpr.right); 24 | this.emit(')'); 25 | break; 26 | case ts.SyntaxKind.CaretToken: 27 | case ts.SyntaxKind.BarToken: 28 | case ts.SyntaxKind.AmpersandToken: 29 | case ts.SyntaxKind.GreaterThanGreaterThanToken: 30 | case ts.SyntaxKind.LessThanLessThanToken: 31 | case ts.SyntaxKind.CaretEqualsToken: 32 | case ts.SyntaxKind.BarEqualsToken: 33 | case ts.SyntaxKind.AmpersandEqualsToken: 34 | case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken: 35 | case ts.SyntaxKind.LessThanLessThanEqualsToken: 36 | // In Dart, the bitwise operators are only available on int, so the number types ts2dart 37 | // deals with have to be converted to int explicitly to match JS's semantics in Dart. 38 | if (tokenStr[tokenStr.length - 1] === '=') { 39 | // For assignments, strip the trailing `=` sign to emit just the operator itself. 40 | this.visit(binExpr.left); 41 | this.emit('='); 42 | this.visitAndWrapAsInt(binExpr.left); 43 | this.emit(tokenStr.slice(0, -1)); 44 | } else { 45 | // normal case (LHS [op]) 46 | this.visitAndWrapAsInt(binExpr.left); 47 | this.emit(tokenStr); 48 | } 49 | this.visitAndWrapAsInt(binExpr.right); 50 | break; 51 | case ts.SyntaxKind.InKeyword: 52 | this.reportError(node, 'in operator is unsupported'); 53 | break; 54 | case ts.SyntaxKind.InstanceOfKeyword: 55 | this.visit(binExpr.left); 56 | this.emit('is'); 57 | this.fc.visitTypeName(binExpr.right); 58 | break; 59 | default: 60 | this.visit(binExpr.left); 61 | this.emit(tokenStr); 62 | this.visit(binExpr.right); 63 | break; 64 | } 65 | break; 66 | case ts.SyntaxKind.PrefixUnaryExpression: 67 | let prefixUnary = node; 68 | let operator = ts.tokenToString(prefixUnary.operator); 69 | this.emit(operator); 70 | 71 | if (prefixUnary.operator === ts.SyntaxKind.TildeToken) { 72 | this.visitAndWrapAsInt(prefixUnary.operand); 73 | } else { 74 | this.visit(prefixUnary.operand); 75 | } 76 | break; 77 | case ts.SyntaxKind.PostfixUnaryExpression: 78 | let postfixUnary = node; 79 | this.visit(postfixUnary.operand); 80 | this.emit(ts.tokenToString(postfixUnary.operator)); 81 | break; 82 | case ts.SyntaxKind.ConditionalExpression: 83 | let conditional = node; 84 | this.visit(conditional.condition); 85 | this.emit('?'); 86 | this.visit(conditional.whenTrue); 87 | this.emit(':'); 88 | this.visit(conditional.whenFalse); 89 | break; 90 | case ts.SyntaxKind.DeleteExpression: 91 | this.reportError(node, 'delete operator is unsupported'); 92 | break; 93 | case ts.SyntaxKind.VoidExpression: 94 | this.reportError(node, 'void operator is unsupported'); 95 | break; 96 | case ts.SyntaxKind.TypeOfExpression: 97 | this.reportError(node, 'typeof operator is unsupported'); 98 | break; 99 | 100 | case ts.SyntaxKind.ParenthesizedExpression: 101 | let parenExpr = node; 102 | this.emit('('); 103 | this.visit(parenExpr.expression); 104 | this.emit(')'); 105 | break; 106 | 107 | case ts.SyntaxKind.PropertyAccessExpression: 108 | let propAccess = node; 109 | if (propAccess.name.text === 'stack' && 110 | this.hasAncestor(propAccess, ts.SyntaxKind.CatchClause)) { 111 | // Handle `e.stack` accesses in catch clauses by mangling to `e_stack`. 112 | // FIXME: Use type checker/FacadeConverter to make sure this is actually Error.stack. 113 | this.visit(propAccess.expression); 114 | this.emitNoSpace('_stack'); 115 | } else { 116 | if (this.fc.handlePropertyAccess(propAccess)) break; 117 | this.visit(propAccess.expression); 118 | this.emit('.'); 119 | this.visit(propAccess.name); 120 | } 121 | break; 122 | case ts.SyntaxKind.ElementAccessExpression: 123 | let elemAccess = node; 124 | this.visit(elemAccess.expression); 125 | this.emit('['); 126 | this.visit(elemAccess.argumentExpression); 127 | this.emit(']'); 128 | break; 129 | 130 | default: 131 | return false; 132 | } 133 | return true; 134 | } 135 | 136 | visitAndWrapAsInt(n: ts.Expression) { 137 | let lhsIsHexLit = n.kind === ts.SyntaxKind.NumericLiteral; 138 | if (lhsIsHexLit) { 139 | this.visit(n); 140 | return; 141 | } 142 | this.emit('('); 143 | this.visit(n); 144 | this.emit('as int)'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/facade_converter.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | 4 | import * as base from './base'; 5 | import {Transpiler} from './main'; 6 | 7 | type CallHandler = (c: ts.CallExpression, context: ts.Expression) => void; 8 | type PropertyHandler = (c: ts.PropertyAccessExpression) => void; 9 | type Set = { 10 | [s: string]: boolean 11 | }; 12 | 13 | const FACADE_DEBUG = false; 14 | 15 | const DEFAULT_LIB_MARKER = '__ts2dart_default_lib'; 16 | const PROVIDER_IMPORT_MARKER = '__ts2dart_has_provider_import'; 17 | const TS2DART_PROVIDER_COMMENT = '@ts2dart_Provider'; 18 | 19 | function merge(...args: {[key: string]: any}[]): {[key: string]: any} { 20 | let returnObject: {[key: string]: any} = {}; 21 | for (let arg of args) { 22 | for (let key of Object.getOwnPropertyNames(arg)) { 23 | returnObject[key] = arg[key]; 24 | } 25 | } 26 | return returnObject; 27 | } 28 | 29 | export class FacadeConverter extends base.TranspilerBase { 30 | private tc: ts.TypeChecker; 31 | private defaultLibLocation: string; 32 | private candidateProperties: {[propertyName: string]: boolean} = {}; 33 | private candidateTypes: {[typeName: string]: boolean} = {}; 34 | private genericMethodDeclDepth = 0; 35 | 36 | constructor(transpiler: Transpiler) { 37 | super(transpiler); 38 | 39 | this.extractPropertyNames(this.callHandlers, this.candidateProperties); 40 | this.extractPropertyNames(this.propertyHandlers, this.candidateProperties); 41 | this.extractPropertyNames(this.tsToDartTypeNames, this.candidateTypes); 42 | } 43 | 44 | initializeTypeBasedConversion( 45 | tc: ts.TypeChecker, opts: ts.CompilerOptions, host: ts.CompilerHost) { 46 | this.tc = tc; 47 | this.defaultLibLocation = ts.getDefaultLibFilePath(opts).replace(/\.d\.ts$/, ''); 48 | this.resolveModuleNames(opts, host, this.callHandlers); 49 | this.resolveModuleNames(opts, host, this.propertyHandlers); 50 | this.resolveModuleNames(opts, host, this.tsToDartTypeNames); 51 | this.resolveModuleNames(opts, host, this.callHandlerReplaceNew); 52 | } 53 | 54 | private extractPropertyNames(m: ts.Map>, candidates: {[k: string]: boolean}) { 55 | for (let fileName of Object.keys(m)) { 56 | const file = m[fileName]; 57 | Object.keys(file) 58 | .map((propName) => propName.substring(propName.lastIndexOf('.') + 1)) 59 | .forEach((propName) => candidates[propName] = true); 60 | } 61 | } 62 | 63 | private resolveModuleNames( 64 | opts: ts.CompilerOptions, host: ts.CompilerHost, m: ts.Map>) { 65 | for (let mn of Object.keys(m)) { 66 | let actual: string; 67 | let absolute: string; 68 | if (mn === DEFAULT_LIB_MARKER) { 69 | actual = this.defaultLibLocation; 70 | } else { 71 | let resolved = ts.resolveModuleName(mn, '', opts, host); 72 | if (!resolved.resolvedModule) continue; 73 | actual = resolved.resolvedModule.resolvedFileName.replace(/(\.d)?\.ts$/, ''); 74 | // TypeScript's resolution returns relative paths here, but uses absolute ones in 75 | // SourceFile.fileName later. Make sure to hit both use cases. 76 | absolute = path.resolve(actual); 77 | } 78 | if (FACADE_DEBUG) console.log('Resolved module', mn, '->', actual); 79 | m[actual] = m[mn]; 80 | if (absolute) m[absolute] = m[mn]; 81 | } 82 | } 83 | 84 | /** 85 | * To avoid strongly referencing the Provider class (which could bloat binary size), Angular 2 86 | * write providers as object literals. However the Dart transformers don't recognize this, so 87 | * ts2dart translates the special syntax `/* @ts2dart_Provider * / {provide: Class, param1: ...}` 88 | * into `const Provider(Class, param1: ...)`. 89 | */ 90 | maybeHandleProvider(ole: ts.ObjectLiteralExpression): boolean { 91 | if (!this.hasMarkerComment(ole, TS2DART_PROVIDER_COMMENT)) return false; 92 | let classParam: ts.Expression; 93 | let remaining = ole.properties.filter((e) => { 94 | if (e.kind !== ts.SyntaxKind.PropertyAssignment) { 95 | this.reportError(e, TS2DART_PROVIDER_COMMENT + ' elements must be property assignments'); 96 | } 97 | if ('provide' === base.ident(e.name)) { 98 | classParam = (e as ts.PropertyAssignment).initializer; 99 | return false; 100 | } 101 | return true; // include below. 102 | }); 103 | 104 | if (!classParam) { 105 | this.reportError(ole, 'missing provide: element'); 106 | return false; 107 | } 108 | 109 | this.emit('const Provider('); 110 | this.visit(classParam); 111 | if (remaining.length > 0) { 112 | this.emit(','); 113 | for (let i = 0; i < remaining.length; i++) { 114 | let e = remaining[i]; 115 | if (e.kind !== ts.SyntaxKind.PropertyAssignment) this.visit(e.name); 116 | this.emit(base.ident(e.name)); 117 | this.emit(':'); 118 | this.visit((e as ts.PropertyAssignment).initializer); 119 | if ((i + 1) < remaining.length) this.emit(','); 120 | } 121 | this.emit(')'); 122 | } 123 | return true; 124 | } 125 | 126 | maybeHandleCall(c: ts.CallExpression): boolean { 127 | if (!this.tc) return false; 128 | let {context, symbol} = this.getCallInformation(c); 129 | if (!symbol) { 130 | // getCallInformation returns a symbol if we understand this call. 131 | return false; 132 | } 133 | let handler = this.getHandler(c, symbol, this.callHandlers); 134 | return handler && !handler(c, context); 135 | } 136 | 137 | handlePropertyAccess(pa: ts.PropertyAccessExpression): boolean { 138 | if (!this.tc) return; 139 | let ident = pa.name.text; 140 | if (!this.candidateProperties.hasOwnProperty(ident)) return false; 141 | let symbol = this.tc.getSymbolAtLocation(pa.name); 142 | if (!symbol) { 143 | this.reportMissingType(pa, ident); 144 | return false; 145 | } 146 | 147 | let handler = this.getHandler(pa, symbol, this.propertyHandlers); 148 | return handler && !handler(pa); 149 | } 150 | 151 | /** 152 | * Searches for type references that require extra imports and emits the imports as necessary. 153 | */ 154 | emitExtraImports(sourceFile: ts.SourceFile) { 155 | let libraries = >{ 156 | 'XMLHttpRequest': 'dart:html', 157 | 'KeyboardEvent': 'dart:html', 158 | 'Uint8Array': 'dart:typed_arrays', 159 | 'ArrayBuffer': 'dart:typed_arrays', 160 | 'Promise': 'dart:async', 161 | }; 162 | let emitted: Set = {}; 163 | this.emitImports(sourceFile, libraries, emitted, sourceFile); 164 | } 165 | 166 | private emitImports( 167 | n: ts.Node, libraries: ts.Map, emitted: Set, sourceFile: ts.SourceFile): void { 168 | if (n.kind === ts.SyntaxKind.TypeReference) { 169 | let type = base.ident((n).typeName); 170 | if (libraries.hasOwnProperty(type)) { 171 | let toEmit = libraries[type]; 172 | if (!emitted[toEmit]) { 173 | this.emit(`import "${toEmit}";`); 174 | emitted[toEmit] = true; 175 | } 176 | } 177 | } 178 | 179 | // Support for importing "Provider" in case /* @ts2dart_Provider */ comments are present. 180 | if (n.kind === ts.SyntaxKind.ImportDeclaration) { 181 | // See if there is already code importing 'Provider' from angular2/core. 182 | let id = n as ts.ImportDeclaration; 183 | if ((id.moduleSpecifier as ts.StringLiteral).text === 'angular2/core') { 184 | if (id.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { 185 | let ni = id.importClause.namedBindings as ts.NamedImports; 186 | for (let nb of ni.elements) { 187 | if (base.ident(nb.name) === 'Provider') { 188 | emitted[PROVIDER_IMPORT_MARKER] = true; 189 | break; 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | if (!emitted[PROVIDER_IMPORT_MARKER] && this.hasMarkerComment(n, TS2DART_PROVIDER_COMMENT)) { 197 | // if 'Provider' has not been imported yet, and there's a @ts2dart_Provider, add it. 198 | this.emit(`import "package:angular2/core.dart" show Provider;`); 199 | emitted[PROVIDER_IMPORT_MARKER] = true; 200 | } 201 | 202 | n.getChildren(sourceFile) 203 | .forEach((child: ts.Node) => this.emitImports(child, libraries, emitted, sourceFile)); 204 | } 205 | 206 | pushTypeParameterNames(n: ts.FunctionLikeDeclaration) { 207 | if (!n.typeParameters) return; 208 | this.genericMethodDeclDepth++; 209 | } 210 | 211 | popTypeParameterNames(n: ts.FunctionLikeDeclaration) { 212 | if (!n.typeParameters) return; 213 | this.genericMethodDeclDepth--; 214 | } 215 | 216 | resolvePropertyTypes(tn: ts.TypeNode): ts.Map { 217 | let res: ts.Map = {}; 218 | if (!tn || !this.tc) return res; 219 | 220 | let t = this.tc.getTypeAtLocation(tn); 221 | for (let sym of this.tc.getPropertiesOfType(t)) { 222 | let decl = sym.valueDeclaration || (sym.declarations && sym.declarations[0]); 223 | if (decl.kind !== ts.SyntaxKind.PropertyDeclaration && 224 | decl.kind !== ts.SyntaxKind.PropertySignature) { 225 | let msg = this.tc.getFullyQualifiedName(sym) + 226 | ' used for named parameter definition must be a property'; 227 | this.reportError(decl, msg); 228 | continue; 229 | } 230 | res[sym.name] = decl; 231 | } 232 | return res; 233 | } 234 | 235 | /** 236 | * The Dart Development Compiler (DDC) has a syntax extension that uses comments to emulate 237 | * generic methods in Dart. ts2dart has to hack around this and keep track of which type names 238 | * in the current scope are actually DDC type parameters and need to be emitted in comments. 239 | * 240 | * TODO(martinprobst): Remove this once the DDC hack has made it into Dart proper. 241 | */ 242 | private isGenericMethodTypeParameterName(name: ts.EntityName): boolean { 243 | // Avoid checking this unless needed. 244 | if (this.genericMethodDeclDepth === 0 || !this.tc) return false; 245 | // Check if the type of the name is a TypeParameter. 246 | let t = this.tc.getTypeAtLocation(name); 247 | if (!t || (t.flags & ts.TypeFlags.TypeParameter) === 0) return false; 248 | 249 | // Check if the symbol we're looking at is the type parameter. 250 | let symbol = this.tc.getSymbolAtLocation(name); 251 | if (symbol !== t.symbol) return false; 252 | 253 | // Check that the Type Parameter has been declared by a function declaration. 254 | return symbol.declarations.some( 255 | // Constructors are handled separately. 256 | d => d.parent.kind === ts.SyntaxKind.FunctionDeclaration || 257 | d.parent.kind === ts.SyntaxKind.MethodDeclaration || 258 | d.parent.kind === ts.SyntaxKind.MethodSignature); 259 | } 260 | 261 | visitTypeName(typeName: ts.EntityName) { 262 | if (typeName.kind !== ts.SyntaxKind.Identifier) { 263 | this.visit(typeName); 264 | return; 265 | } 266 | let ident = base.ident(typeName); 267 | if (this.isGenericMethodTypeParameterName(typeName)) { 268 | // DDC generic methods hack - all names that are type parameters to generic methods have to be 269 | // emitted in comments. 270 | this.emit('dynamic/*='); 271 | this.emit(ident); 272 | this.emit('*/'); 273 | return; 274 | } 275 | 276 | if (this.candidateTypes.hasOwnProperty(ident) && this.tc) { 277 | let symbol = this.tc.getSymbolAtLocation(typeName); 278 | if (!symbol) { 279 | this.reportMissingType(typeName, ident); 280 | return; 281 | } 282 | let fileAndName = this.getFileAndName(typeName, symbol); 283 | if (fileAndName) { 284 | let fileSubs = this.tsToDartTypeNames[fileAndName.fileName]; 285 | if (fileSubs && fileSubs.hasOwnProperty(fileAndName.qname)) { 286 | this.emit(fileSubs[fileAndName.qname]); 287 | return; 288 | } 289 | } 290 | } 291 | this.emit(ident); 292 | } 293 | 294 | shouldEmitNew(c: ts.CallExpression): boolean { 295 | if (!this.tc) return true; 296 | 297 | let ci = this.getCallInformation(c); 298 | let symbol = ci.symbol; 299 | // getCallInformation returns a symbol if we understand this call. 300 | if (!symbol) return true; 301 | 302 | let loc = this.getFileAndName(c, symbol); 303 | if (!loc) return true; 304 | let {fileName, qname} = loc; 305 | let fileSubs = this.callHandlerReplaceNew[fileName]; 306 | if (!fileSubs) return true; 307 | return !fileSubs[qname]; 308 | } 309 | 310 | private getCallInformation(c: ts.CallExpression): {context?: ts.Expression, symbol?: ts.Symbol} { 311 | let symbol: ts.Symbol; 312 | let context: ts.Expression; 313 | let ident: string; 314 | let expr = c.expression; 315 | 316 | if (expr.kind === ts.SyntaxKind.Identifier) { 317 | // Function call. 318 | ident = base.ident(expr); 319 | if (!this.candidateProperties.hasOwnProperty(ident)) return {}; 320 | symbol = this.tc.getSymbolAtLocation(expr); 321 | 322 | if (!symbol) { 323 | this.reportMissingType(c, ident); 324 | return {}; 325 | } 326 | 327 | context = null; 328 | } else if (expr.kind === ts.SyntaxKind.PropertyAccessExpression) { 329 | // Method call. 330 | let pa = expr; 331 | ident = base.ident(pa.name); 332 | if (!this.candidateProperties.hasOwnProperty(ident)) return {}; 333 | 334 | symbol = this.tc.getSymbolAtLocation(pa); 335 | 336 | // Error will be reported by PropertyAccess handling below. 337 | if (!symbol) return {}; 338 | 339 | context = pa.expression; 340 | } 341 | return {context, symbol}; 342 | } 343 | 344 | private getHandler(n: ts.Node, symbol: ts.Symbol, m: ts.Map>): T { 345 | let loc = this.getFileAndName(n, symbol); 346 | if (!loc) return null; 347 | let {fileName, qname} = loc; 348 | let fileSubs = m[fileName]; 349 | if (!fileSubs) return null; 350 | return fileSubs[qname]; 351 | } 352 | 353 | private getFileAndName(n: ts.Node, originalSymbol: ts.Symbol): {fileName: string, qname: string} { 354 | let symbol = originalSymbol; 355 | while (symbol.flags & ts.SymbolFlags.Alias) symbol = this.tc.getAliasedSymbol(symbol); 356 | let decl = symbol.valueDeclaration; 357 | if (!decl) { 358 | // In the case of a pure declaration with no assignment, there is no value declared. 359 | // Just grab the first declaration, hoping it is declared once. 360 | if (!symbol.declarations || symbol.declarations.length === 0) { 361 | this.reportError(n, 'no declarations for symbol ' + originalSymbol.name); 362 | return null; 363 | } 364 | decl = symbol.declarations[0]; 365 | } 366 | 367 | const canonicalFileName = decl.getSourceFile().fileName.replace(/(\.d)?\.ts$/, ''); 368 | 369 | let qname = this.tc.getFullyQualifiedName(symbol); 370 | // Some Qualified Names include their file name. Might be a bug in TypeScript, 371 | // for the time being just special case. 372 | if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) { 373 | qname = symbol.getName(); 374 | } 375 | if (FACADE_DEBUG) console.error('cfn:', canonicalFileName, 'qn:', qname); 376 | return {fileName: canonicalFileName, qname}; 377 | } 378 | 379 | private isNamedDefaultLibType(node: ts.Node, qname: string): boolean { 380 | let symbol = this.tc.getTypeAtLocation(node).getSymbol(); 381 | if (!symbol) return false; 382 | let actual = this.getFileAndName(node, symbol); 383 | return actual.fileName === this.defaultLibLocation && qname === actual.qname; 384 | } 385 | 386 | private reportMissingType(n: ts.Node, ident: string) { 387 | this.reportError( 388 | n, `Untyped property access to "${ident}" which could be ` + 389 | `a special ts2dart builtin. ` + 390 | `Please add type declarations to disambiguate.`); 391 | } 392 | 393 | private static DECLARATIONS: {[k: number]: boolean} = { 394 | [ts.SyntaxKind.ClassDeclaration]: true, 395 | [ts.SyntaxKind.FunctionDeclaration]: true, 396 | [ts.SyntaxKind.InterfaceDeclaration]: true, 397 | [ts.SyntaxKind.MethodDeclaration]: true, 398 | [ts.SyntaxKind.PropertyDeclaration]: true, 399 | [ts.SyntaxKind.PropertyDeclaration]: true, 400 | [ts.SyntaxKind.VariableDeclaration]: true, 401 | }; 402 | 403 | isInsideConstExpr(node: ts.Node): boolean { 404 | while (node.parent) { 405 | if (node.parent.kind === ts.SyntaxKind.Parameter && 406 | (node.parent as ts.ParameterDeclaration).initializer === node) { 407 | // initializers of parameters must be const in Dart. 408 | return true; 409 | } 410 | if (this.isConstExpr(node)) return true; 411 | node = node.parent; 412 | if (FacadeConverter.DECLARATIONS[node.kind]) { 413 | // Stop walking upwards when hitting a declaration - @ts2dart_const should only propagate 414 | // to the immediate declaration it applies to (but should be transitive in expressions). 415 | return false; 416 | } 417 | } 418 | return false; 419 | } 420 | 421 | isConstClass(decl: base.ClassLike) { 422 | return this.hasConstComment(decl) || this.hasAnnotation(decl.decorators, 'CONST') || 423 | (>decl.members).some((m) => { 424 | if (m.kind !== ts.SyntaxKind.Constructor) return false; 425 | return this.hasAnnotation(m.decorators, 'CONST'); 426 | }); 427 | } 428 | 429 | /** 430 | * isConstExpr returns true if the passed in expression itself is a const expression. const 431 | * expressions are marked by the special comment @ts2dart_const (expr), or by the special 432 | * function call CONST_EXPR. 433 | */ 434 | isConstExpr(node: ts.Node): boolean { 435 | if (!node) return false; 436 | 437 | if (this.hasConstComment(node)) { 438 | return true; 439 | } 440 | 441 | return node.kind === ts.SyntaxKind.CallExpression && 442 | base.ident((node).expression) === 'CONST_EXPR'; 443 | } 444 | 445 | hasConstComment(node: ts.Node): boolean { return this.hasMarkerComment(node, '@ts2dart_const'); } 446 | 447 | private hasMarkerComment(node: ts.Node, markerText: string): boolean { 448 | let text = node.getFullText(); 449 | let comments = ts.getLeadingCommentRanges(text, 0); 450 | if (!comments) return false; 451 | for (let c of comments) { 452 | let commentText = text.substring(c.pos, c.end); 453 | if (commentText.indexOf(markerText) !== -1) { 454 | return true; 455 | } 456 | } 457 | return false; 458 | } 459 | 460 | private emitMethodCall(name: string, args?: ts.Expression[]) { 461 | this.emit('.'); 462 | this.emitCall(name, args); 463 | } 464 | 465 | private emitCall(name: string, args?: ts.Expression[]) { 466 | this.emit(name); 467 | this.emit('('); 468 | if (args) this.visitList(args); 469 | this.emit(')'); 470 | } 471 | 472 | private stdlibTypeReplacements: ts.Map = { 473 | 'Date': 'DateTime', 474 | 'Array': 'List', 475 | 'XMLHttpRequest': 'HttpRequest', 476 | 'Uint8Array': 'Uint8List', 477 | 'ArrayBuffer': 'ByteBuffer', 478 | 'Promise': 'Future', 479 | 480 | // Dart has two different incompatible DOM APIs 481 | // https://github.com/angular/angular/issues/2770 482 | 'Node': 'dynamic', 483 | 'Text': 'dynamic', 484 | 'Element': 'dynamic', 485 | 'Event': 'dynamic', 486 | 'HTMLElement': 'dynamic', 487 | 'HTMLAnchorElement': 'dynamic', 488 | 'HTMLStyleElement': 'dynamic', 489 | 'HTMLInputElement': 'dynamic', 490 | 'HTMLDocument': 'dynamic', 491 | 'History': 'dynamic', 492 | 'Location': 'dynamic', 493 | }; 494 | 495 | private tsToDartTypeNames: ts.Map> = { 496 | [DEFAULT_LIB_MARKER]: this.stdlibTypeReplacements, 497 | 'angular2/src/facade/lang': {'Date': 'DateTime'}, 498 | 499 | 'rxjs/Observable': {'Observable': 'Stream'}, 500 | 'es6-promise/es6-promise': {'Promise': 'Future'}, 501 | 'es6-shim/es6-shim': {'Promise': 'Future'}, 502 | }; 503 | 504 | private es6Promises: ts.Map = { 505 | 'Promise.catch': (c: ts.CallExpression, context: ts.Expression) => { 506 | this.visit(context); 507 | this.emit('.catchError('); 508 | this.visitList(c.arguments); 509 | this.emit(')'); 510 | }, 511 | 'Promise.then': (c: ts.CallExpression, context: ts.Expression) => { 512 | // then() in Dart doesn't support 2 arguments. 513 | this.visit(context); 514 | this.emit('.then('); 515 | this.visit(c.arguments[0]); 516 | this.emit(')'); 517 | if (c.arguments.length > 1) { 518 | this.emit('.catchError('); 519 | this.visit(c.arguments[1]); 520 | this.emit(')'); 521 | } 522 | }, 523 | 'Promise': (c: ts.CallExpression, context: ts.Expression) => { 524 | if (c.kind !== ts.SyntaxKind.NewExpression) return true; 525 | this.assert(c, c.arguments.length === 1, 'Promise construction must take 2 arguments.'); 526 | this.assert( 527 | c, c.arguments[0].kind === ts.SyntaxKind.ArrowFunction || 528 | c.arguments[0].kind === ts.SyntaxKind.FunctionExpression, 529 | 'Promise argument must be a function expression (or arrow function).'); 530 | let callback: ts.FunctionLikeDeclaration; 531 | if (c.arguments[0].kind === ts.SyntaxKind.ArrowFunction) { 532 | callback = (c.arguments[0]); 533 | } else if (c.arguments[0].kind === ts.SyntaxKind.FunctionExpression) { 534 | callback = (c.arguments[0]); 535 | } 536 | this.assert( 537 | c, callback.parameters.length > 0 && callback.parameters.length < 3, 538 | 'Promise executor must take 1 or 2 arguments (resolve and reject).'); 539 | 540 | const completerVarName = this.uniqueId('completer'); 541 | this.assert( 542 | c, callback.parameters[0].name.kind === ts.SyntaxKind.Identifier, 543 | 'First argument of the Promise executor is not a straight parameter.'); 544 | let resolveParameterIdent = (callback.parameters[0].name); 545 | 546 | this.emit('(() {'); // Create a new scope. 547 | this.emit(`Completer ${completerVarName} = new Completer();`); 548 | this.emit('var'); 549 | this.emit(resolveParameterIdent.text); 550 | this.emit(`= ${completerVarName}.complete;`); 551 | 552 | if (callback.parameters.length === 2) { 553 | this.assert( 554 | c, callback.parameters[1].name.kind === ts.SyntaxKind.Identifier, 555 | 'First argument of the Promise executor is not a straight parameter.'); 556 | let rejectParameterIdent = (callback.parameters[1].name); 557 | this.emit('var'); 558 | this.emit(rejectParameterIdent.text); 559 | this.emit(`= ${completerVarName}.completeError;`); 560 | } 561 | this.emit('(()'); 562 | this.visit(callback.body); 563 | this.emit(')();'); 564 | this.emit(`return ${completerVarName}.future;`); 565 | this.emit('})()'); 566 | }, 567 | }; 568 | 569 | private es6Collections: ts.Map = { 570 | 'Map.set': (c: ts.CallExpression, context: ts.Expression) => { 571 | this.visit(context); 572 | this.emit('['); 573 | this.visit(c.arguments[0]); 574 | this.emit(']'); 575 | this.emit('='); 576 | this.visit(c.arguments[1]); 577 | }, 578 | 'Map.get': (c: ts.CallExpression, context: ts.Expression) => { 579 | this.visit(context); 580 | this.emit('['); 581 | this.visit(c.arguments[0]); 582 | this.emit(']'); 583 | }, 584 | 'Map.has': (c: ts.CallExpression, context: ts.Expression) => { 585 | this.visit(context); 586 | this.emitMethodCall('containsKey', c.arguments); 587 | }, 588 | 'Map.delete': (c: ts.CallExpression, context: ts.Expression) => { 589 | // JS Map.delete(k) returns whether k was present in the map, 590 | // convert to: 591 | // (Map.containsKey(k) && (Map.remove(k) !== null || true)) 592 | // (Map.remove(k) !== null || true) is required to always returns true 593 | // when Map.containsKey(k) 594 | this.emit('('); 595 | this.visit(context); 596 | this.emitMethodCall('containsKey', c.arguments); 597 | this.emit('&& ('); 598 | this.visit(context); 599 | this.emitMethodCall('remove', c.arguments); 600 | this.emit('!= null || true ) )'); 601 | }, 602 | 'Map.forEach': (c: ts.CallExpression, context: ts.Expression) => { 603 | let cb: any; 604 | let params: any; 605 | 606 | switch (c.arguments[0].kind) { 607 | case ts.SyntaxKind.FunctionExpression: 608 | cb = (c.arguments[0]); 609 | params = cb.parameters; 610 | if (params.length !== 2) { 611 | this.reportError(c, 'Map.forEach callback requires exactly two arguments'); 612 | return; 613 | } 614 | this.visit(context); 615 | this.emit('. forEach ( ('); 616 | this.visit(params[1]); 617 | this.emit(','); 618 | this.visit(params[0]); 619 | this.emit(')'); 620 | this.visit(cb.body); 621 | this.emit(')'); 622 | break; 623 | 624 | case ts.SyntaxKind.ArrowFunction: 625 | cb = (c.arguments[0]); 626 | params = cb.parameters; 627 | if (params.length !== 2) { 628 | this.reportError(c, 'Map.forEach callback requires exactly two arguments'); 629 | return; 630 | } 631 | this.visit(context); 632 | this.emit('. forEach ( ('); 633 | this.visit(params[1]); 634 | this.emit(','); 635 | this.visit(params[0]); 636 | this.emit(')'); 637 | if (cb.body.kind !== ts.SyntaxKind.Block) { 638 | this.emit('=>'); 639 | } 640 | this.visit(cb.body); 641 | this.emit(')'); 642 | break; 643 | 644 | default: 645 | this.visit(context); 646 | this.emit('. forEach ( ( k , v ) => ('); 647 | this.visit(c.arguments[0]); 648 | this.emit(') ( v , k ) )'); 649 | break; 650 | } 651 | }, 652 | 'Array.find': (c: ts.CallExpression, context: ts.Expression) => { 653 | this.visit(context); 654 | this.emit('. firstWhere ('); 655 | this.visit(c.arguments[0]); 656 | this.emit(', orElse : ( ) => null )'); 657 | }, 658 | }; 659 | 660 | private stdlibHandlers: ts.Map = merge(this.es6Promises, this.es6Collections, { 661 | 'Array.push': (c: ts.CallExpression, context: ts.Expression) => { 662 | this.visit(context); 663 | this.emitMethodCall('add', c.arguments); 664 | }, 665 | 'Array.pop': (c: ts.CallExpression, context: ts.Expression) => { 666 | this.visit(context); 667 | this.emitMethodCall('removeLast'); 668 | }, 669 | 'Array.shift': (c: ts.CallExpression, context: ts.Expression) => { 670 | this.visit(context); 671 | this.emit('. removeAt ( 0 )'); 672 | }, 673 | 'Array.unshift': (c: ts.CallExpression, context: ts.Expression) => { 674 | this.emit('('); 675 | this.visit(context); 676 | if (c.arguments.length === 1) { 677 | this.emit('.. insert ( 0,'); 678 | this.visit(c.arguments[0]); 679 | this.emit(') ) . length'); 680 | } else { 681 | this.emit('.. insertAll ( 0, ['); 682 | this.visitList(c.arguments); 683 | this.emit(']) ) . length'); 684 | } 685 | }, 686 | 'Array.map': (c: ts.CallExpression, context: ts.Expression) => { 687 | this.visit(context); 688 | this.emitMethodCall('map', c.arguments); 689 | this.emitMethodCall('toList'); 690 | }, 691 | 'Array.filter': (c: ts.CallExpression, context: ts.Expression) => { 692 | this.visit(context); 693 | this.emitMethodCall('where', c.arguments); 694 | this.emitMethodCall('toList'); 695 | }, 696 | 'Array.some': (c: ts.CallExpression, context: ts.Expression) => { 697 | this.visit(context); 698 | this.emitMethodCall('any', c.arguments); 699 | }, 700 | 'Array.slice': (c: ts.CallExpression, context: ts.Expression) => { 701 | this.emitCall('ListWrapper.slice', [context, ...c.arguments]); 702 | }, 703 | 'Array.splice': (c: ts.CallExpression, context: ts.Expression) => { 704 | this.emitCall('ListWrapper.splice', [context, ...c.arguments]); 705 | }, 706 | 'Array.concat': (c: ts.CallExpression, context: ts.Expression) => { 707 | this.emit('( new List . from ('); 708 | this.visit(context); 709 | this.emit(')'); 710 | c.arguments.forEach(arg => { 711 | if (!this.isNamedDefaultLibType(arg, 'Array')) { 712 | this.reportError(arg, 'Array.concat only takes Array arguments'); 713 | } 714 | this.emit('.. addAll ('); 715 | this.visit(arg); 716 | this.emit(')'); 717 | }); 718 | this.emit(')'); 719 | }, 720 | 'Array.join': (c: ts.CallExpression, context: ts.Expression) => { 721 | this.visit(context); 722 | if (c.arguments.length) { 723 | this.emitMethodCall('join', c.arguments); 724 | } else { 725 | this.emit('. join ( "," )'); 726 | } 727 | }, 728 | 'Array.reduce': (c: ts.CallExpression, context: ts.Expression) => { 729 | this.visit(context); 730 | 731 | if (c.arguments.length >= 2) { 732 | this.emitMethodCall('fold', [c.arguments[1], c.arguments[0]]); 733 | } else { 734 | this.emit('. fold ( null ,'); 735 | this.visit(c.arguments[0]); 736 | this.emit(')'); 737 | } 738 | }, 739 | 'ArrayConstructor.isArray': (c: ts.CallExpression, context: ts.Expression) => { 740 | this.emit('( ('); 741 | this.visitList(c.arguments); // Should only be 1. 742 | this.emit(')'); 743 | this.emit('is List'); 744 | this.emit(')'); 745 | }, 746 | 'Console.log': (c: ts.CallExpression, context: ts.Expression) => { 747 | this.emit('print('); 748 | if (c.arguments.length === 1) { 749 | this.visit(c.arguments[0]); 750 | } else { 751 | this.emit('['); 752 | this.visitList(c.arguments); 753 | this.emit('].join(" ")'); 754 | } 755 | this.emit(')'); 756 | }, 757 | 'RegExp.exec': (c: ts.CallExpression, context: ts.Expression) => { 758 | if (context.kind !== ts.SyntaxKind.RegularExpressionLiteral) { 759 | // Fail if the exec call isn't made directly on a regexp literal. 760 | // Multiple exec calls on the same global regexp have side effects 761 | // (each return the next match), which we can't reproduce with a simple 762 | // Dart RegExp (users should switch to some facade / wrapper instead). 763 | this.reportError( 764 | c, 'exec is only supported on regexp literals, ' + 765 | 'to avoid side-effect of multiple calls on global regexps.'); 766 | } 767 | if (c.parent.kind === ts.SyntaxKind.ElementAccessExpression) { 768 | // The result of the exec call is used for immediate indexed access: 769 | // this use-case can be accommodated by RegExp.firstMatch, which returns 770 | // a Match instance with operator[] which returns groups (special index 771 | // 0 returns the full text of the match). 772 | this.visit(context); 773 | this.emitMethodCall('firstMatch', c.arguments); 774 | } else { 775 | // In the general case, we want to return a List. To transform a Match 776 | // into a List of its groups, we alias it in a local closure that we 777 | // call with the Match value. We are then able to use the group method 778 | // to generate a List large enough to hold groupCount groups + the 779 | // full text of the match at special group index 0. 780 | this.emit('((match) => new List.generate(1 + match.groupCount, match.group))('); 781 | this.visit(context); 782 | this.emitMethodCall('firstMatch', c.arguments); 783 | this.emit(')'); 784 | } 785 | }, 786 | 'RegExp.test': (c: ts.CallExpression, context: ts.Expression) => { 787 | this.visit(context); 788 | this.emitMethodCall('hasMatch', c.arguments); 789 | }, 790 | 'String.substr': (c: ts.CallExpression, context: ts.Expression) => { 791 | this.reportError( 792 | c, 'substr is unsupported, use substring (but beware of the different semantics!)'); 793 | this.visit(context); 794 | this.emitMethodCall('substr', c.arguments); 795 | }, 796 | }); 797 | 798 | private callHandlerReplaceNew: ts.Map> = { 799 | [DEFAULT_LIB_MARKER]: {'Promise': true}, 800 | }; 801 | 802 | private callHandlers: ts.Map> = { 803 | [DEFAULT_LIB_MARKER]: this.stdlibHandlers, 804 | 'angular2/manual_typings/globals': this.es6Collections, 805 | 'angular2/src/facade/collection': { 806 | 'Map': (c: ts.CallExpression, context: ts.Expression): boolean => { 807 | // The actual Map constructor is special cased for const calls. 808 | if (!this.isInsideConstExpr(c)) return true; 809 | if (c.arguments.length) { 810 | this.reportError(c, 'Arguments on a Map constructor in a const are unsupported'); 811 | } 812 | if (c.typeArguments) { 813 | this.emit('<'); 814 | this.visitList(c.typeArguments); 815 | this.emit('>'); 816 | } 817 | this.emit('{ }'); 818 | return false; 819 | }, 820 | }, 821 | 'angular2/src/core/di/forward_ref': { 822 | 'forwardRef': (c: ts.CallExpression, context: ts.Expression) => { 823 | // The special function forwardRef translates to an unwrapped value in Dart. 824 | const callback = c.arguments[0]; 825 | if (callback.kind !== ts.SyntaxKind.ArrowFunction) { 826 | this.reportError(c, 'forwardRef takes only arrow functions'); 827 | return; 828 | } 829 | this.visit(callback.body); 830 | }, 831 | }, 832 | 'angular2/src/facade/lang': { 833 | 'CONST_EXPR': (c: ts.CallExpression, context: ts.Expression) => { 834 | // `const` keyword is emitted in the array literal handling, as it needs to be transitive. 835 | this.visitList(c.arguments); 836 | }, 837 | 'normalizeBlank': (c: ts.CallExpression, context: ts.Expression) => { 838 | // normalizeBlank is a noop in Dart, so erase it. 839 | this.visitList(c.arguments); 840 | }, 841 | }, 842 | }; 843 | 844 | private es6CollectionsProp: ts.Map = { 845 | 'Map.size': (p: ts.PropertyAccessExpression) => { 846 | this.visit(p.expression); 847 | this.emit('.'); 848 | this.emit('length'); 849 | }, 850 | }; 851 | private es6PromisesProp: ts.Map = { 852 | 'PromiseConstructor.resolve': (p: ts.PropertyAccessExpression) => { 853 | this.emit('new '); 854 | this.visit(p.expression); 855 | this.emit('.value'); 856 | }, 857 | 'PromiseConstructor.reject': (p: ts.PropertyAccessExpression) => { 858 | this.emit('new '); 859 | this.visit(p.expression); 860 | this.emit('.error'); 861 | }, 862 | }; 863 | 864 | private propertyHandlers: ts.Map> = { 865 | [DEFAULT_LIB_MARKER]: merge(this.es6CollectionsProp, this.es6PromisesProp), 866 | }; 867 | } 868 | -------------------------------------------------------------------------------- /lib/literal.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as base from './base'; 4 | import {FacadeConverter} from './facade_converter'; 5 | import {Transpiler} from './main'; 6 | 7 | export default class LiteralTranspiler extends base.TranspilerBase { 8 | constructor(tr: Transpiler, private fc: FacadeConverter) { super(tr); } 9 | 10 | visitNode(node: ts.Node): boolean { 11 | switch (node.kind) { 12 | // Literals. 13 | case ts.SyntaxKind.NumericLiteral: 14 | let nLit = node; 15 | this.emit(nLit.getText()); 16 | break; 17 | case ts.SyntaxKind.StringLiteral: 18 | let sLit = node; 19 | let text = JSON.stringify(sLit.text); 20 | // Escape dollar sign since dart will interpolate in double quoted literal 21 | text = text.replace(/\$/, '\\$'); 22 | this.emit(text); 23 | break; 24 | case ts.SyntaxKind.NoSubstitutionTemplateLiteral: 25 | this.emit(`'''${this.escapeTextForTemplateString(node)}'''`); 26 | break; 27 | case ts.SyntaxKind.TemplateMiddle: 28 | this.emitNoSpace(this.escapeTextForTemplateString(node)); 29 | break; 30 | case ts.SyntaxKind.TemplateExpression: 31 | let tmpl = node; 32 | if (tmpl.head) this.visit(tmpl.head); 33 | if (tmpl.templateSpans) this.visitEach(tmpl.templateSpans); 34 | break; 35 | case ts.SyntaxKind.TemplateHead: 36 | this.emit(`'''${this.escapeTextForTemplateString(node)}`); // highlighting bug:' 37 | break; 38 | case ts.SyntaxKind.TemplateTail: 39 | this.emitNoSpace(this.escapeTextForTemplateString(node)); 40 | this.emitNoSpace(`'''`); 41 | break; 42 | case ts.SyntaxKind.TemplateSpan: 43 | let span = node; 44 | if (span.expression) { 45 | // Do not emit extra whitespace inside the string template 46 | this.emitNoSpace('${'); 47 | this.visit(span.expression); 48 | this.emitNoSpace('}'); 49 | } 50 | if (span.literal) this.visit(span.literal); 51 | break; 52 | case ts.SyntaxKind.ArrayLiteralExpression: 53 | if (this.shouldBeConst(node)) this.emit('const'); 54 | let ale = node; 55 | this.handleReifiedArray(ale); 56 | this.emit('['); 57 | this.visitList(ale.elements); 58 | this.emit(']'); 59 | break; 60 | case ts.SyntaxKind.ObjectLiteralExpression: 61 | let ole = node; 62 | if (this.fc.maybeHandleProvider(ole)) return true; 63 | if (this.shouldBeConst(node)) this.emit('const'); 64 | this.handleReifiedMap(ole); 65 | this.emit('{'); 66 | this.visitList(ole.properties); 67 | this.emit('}'); 68 | break; 69 | case ts.SyntaxKind.PropertyAssignment: 70 | let propAssign = node; 71 | if (propAssign.name.kind === ts.SyntaxKind.Identifier) { 72 | // Dart identifiers in Map literals need quoting. 73 | this.emitNoSpace(' "'); 74 | this.emitNoSpace((propAssign.name).text); 75 | this.emitNoSpace('"'); 76 | } else { 77 | this.visit(propAssign.name); 78 | } 79 | this.emit(':'); 80 | this.visit(propAssign.initializer); 81 | break; 82 | case ts.SyntaxKind.ShorthandPropertyAssignment: 83 | let shorthand = node; 84 | this.emitNoSpace(' "'); 85 | this.emitNoSpace(shorthand.name.text); 86 | this.emitNoSpace('"'); 87 | this.emit(':'); 88 | this.visit(shorthand.name); 89 | break; 90 | 91 | case ts.SyntaxKind.TrueKeyword: 92 | this.emit('true'); 93 | break; 94 | case ts.SyntaxKind.FalseKeyword: 95 | this.emit('false'); 96 | break; 97 | case ts.SyntaxKind.NullKeyword: 98 | this.emit('null'); 99 | break; 100 | case ts.SyntaxKind.RegularExpressionLiteral: 101 | this.emit('new RegExp ('); 102 | this.emit('r\''); 103 | let regExp = (node).text; 104 | let slashIdx = regExp.lastIndexOf('/'); 105 | let flags = regExp.substring(slashIdx + 1); 106 | regExp = regExp.substring(1, slashIdx); // cut off /.../ chars. 107 | regExp = regExp.replace(/'/g, '\' + "\'" + r\''); // handle nested quotes by concatenation. 108 | this.emitNoSpace(regExp); 109 | this.emitNoSpace('\''); 110 | if (flags.indexOf('g') === -1) { 111 | // Dart RegExps are always global, so JS regexps must use 'g' so that semantics match. 112 | this.reportError(node, 'Regular Expressions must use the //g flag'); 113 | } 114 | if (flags.indexOf('m') !== -1) { 115 | this.emit(', multiLine: true'); 116 | } 117 | if (flags.indexOf('i') !== -1) { 118 | this.emit(', caseSensitive: false'); 119 | } 120 | this.emit(')'); 121 | break; 122 | case ts.SyntaxKind.ThisKeyword: 123 | this.emit('this'); 124 | break; 125 | 126 | default: 127 | return false; 128 | } 129 | return true; 130 | } 131 | 132 | private shouldBeConst(n: ts.Node): boolean { 133 | return this.hasAncestor(n, ts.SyntaxKind.Decorator) || this.fc.isInsideConstExpr(n); 134 | } 135 | 136 | private escapeTextForTemplateString(n: ts.Node): string { 137 | return (n).text.replace(/\\/g, '\\\\').replace(/([$'])/g, '\\$1'); 138 | } 139 | 140 | private handleReifiedArray(node: ts.ArrayLiteralExpression) { 141 | if (node.parent.kind !== ts.SyntaxKind.TypeAssertionExpression) return; 142 | let ta = node.parent; 143 | if (ta.type.kind !== ts.SyntaxKind.ArrayType) return; 144 | this.emit('<'); 145 | this.visit((ta.type).elementType); 146 | this.emit('>'); 147 | return true; 148 | } 149 | 150 | 151 | private handleReifiedMap(node: ts.ObjectLiteralExpression) { 152 | if (node.parent.kind !== ts.SyntaxKind.TypeAssertionExpression) return; 153 | let ta = node.parent; 154 | if (ta.type.kind !== ts.SyntaxKind.TypeLiteral) return; 155 | let it = this.maybeDestructureIndexType(ta.type); 156 | if (!it) { 157 | this.reportError(node, 'expected {[k]: v} type on object literal'); 158 | return; 159 | } 160 | this.emit('<'); 161 | this.visit(it[0]); 162 | this.emit(','); 163 | this.visit(it[1]); 164 | this.emit('>'); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/main.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | require('source-map-support').install(); 4 | import {SourceMapGenerator} from 'source-map'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | import * as ts from 'typescript'; 8 | 9 | import {TranspilerBase} from './base'; 10 | import mkdirP from './mkdirp'; 11 | import CallTranspiler from './call'; 12 | import DeclarationTranspiler from './declaration'; 13 | import ExpressionTranspiler from './expression'; 14 | import ModuleTranspiler from './module'; 15 | import StatementTranspiler from './statement'; 16 | import TypeTranspiler from './type'; 17 | import LiteralTranspiler from './literal'; 18 | import {FacadeConverter} from './facade_converter'; 19 | import * as dartStyle from 'dart-style'; 20 | 21 | export interface TranspilerOptions { 22 | /** 23 | * Fail on the first error, do not collect multiple. Allows easier debugging as stack traces lead 24 | * directly to the offending line. 25 | */ 26 | failFast?: boolean; 27 | /** Whether to generate 'library a.b.c;' names from relative file paths. */ 28 | generateLibraryName?: boolean; 29 | /** Whether to generate source maps. */ 30 | generateSourceMap?: boolean; 31 | /** A tsconfig.json to use to configure TypeScript compilation. */ 32 | tsconfig?: string; 33 | /** 34 | * A base path to relativize absolute file paths against. This is useful for library name 35 | * generation (see above) and nicer file names in error messages. 36 | */ 37 | basePath?: string; 38 | /** 39 | * Translate calls to builtins, i.e. seemlessly convert from `Array` to `List`, and convert the 40 | * corresponding methods. Requires type checking. 41 | */ 42 | translateBuiltins?: boolean; 43 | /** 44 | * Enforce conventions of public/private keyword and underscore prefix 45 | */ 46 | enforceUnderscoreConventions?: boolean; 47 | } 48 | 49 | export const COMPILER_OPTIONS: ts.CompilerOptions = { 50 | allowNonTsExtensions: true, 51 | experimentalDecorators: true, 52 | module: ts.ModuleKind.CommonJS, 53 | target: ts.ScriptTarget.ES6, 54 | }; 55 | 56 | export class Transpiler { 57 | private output: Output; 58 | private currentFile: ts.SourceFile; 59 | 60 | // Comments attach to all following AST nodes before the next 'physical' token. Track the earliest 61 | // offset to avoid printing comments multiple times. 62 | private lastCommentIdx: number = -1; 63 | private errors: string[] = []; 64 | 65 | private transpilers: TranspilerBase[]; 66 | private fc: FacadeConverter; 67 | 68 | constructor(private options: TranspilerOptions = {}) { 69 | // TODO: Remove the angular2 default when angular uses typingsRoot. 70 | this.fc = new FacadeConverter(this); 71 | this.transpilers = [ 72 | new CallTranspiler(this, this.fc), // Has to come before StatementTranspiler! 73 | new DeclarationTranspiler(this, this.fc, options.enforceUnderscoreConventions), 74 | new ExpressionTranspiler(this, this.fc), 75 | new LiteralTranspiler(this, this.fc), 76 | new ModuleTranspiler(this, this.fc, options.generateLibraryName), 77 | new StatementTranspiler(this), 78 | new TypeTranspiler(this, this.fc), 79 | ]; 80 | } 81 | 82 | /** 83 | * Transpiles the given files to Dart. 84 | * @param fileNames The input files. 85 | * @param destination Location to write files to. Creates files next to their sources if absent. 86 | */ 87 | transpile(fileNames: string[], destination?: string): void { 88 | if (this.options.basePath) { 89 | this.options.basePath = this.normalizeSlashes(path.resolve(this.options.basePath)); 90 | } 91 | fileNames = fileNames.map((f) => this.normalizeSlashes(path.resolve(f))); 92 | 93 | let host: ts.CompilerHost; 94 | let compilerOpts: ts.CompilerOptions; 95 | if (this.options.tsconfig) { 96 | let {config, error} = 97 | ts.readConfigFile(this.options.tsconfig, (f) => fs.readFileSync(f, 'utf-8')); 98 | if (error) throw new Error(ts.flattenDiagnosticMessageText(error.messageText, '\n')); 99 | let {options, errors} = ts.convertCompilerOptionsFromJson( 100 | config.compilerOptions, path.dirname(this.options.tsconfig)); 101 | if (errors && errors.length) { 102 | throw new Error(errors.map((d) => this.diagnosticToString(d)).join('\n')); 103 | } 104 | host = ts.createCompilerHost(options, /*setParentNodes*/ true); 105 | compilerOpts = options; 106 | if (compilerOpts.rootDir != null && this.options.basePath == null) { 107 | // Use the tsconfig's rootDir if basePath is not set. 108 | this.options.basePath = compilerOpts.rootDir; 109 | } 110 | if (compilerOpts.outDir != null && destination == null) { 111 | destination = compilerOpts.outDir; 112 | } 113 | } else { 114 | host = this.createCompilerHost(); 115 | compilerOpts = this.getCompilerOptions(); 116 | } 117 | if (this.options.basePath) this.options.basePath = path.resolve(this.options.basePath); 118 | 119 | if (this.options.basePath && destination === undefined) { 120 | throw new Error( 121 | 'Must have a destination path when a basePath is specified ' + this.options.basePath); 122 | } 123 | let destinationRoot = destination || this.options.basePath || ''; 124 | let program = ts.createProgram(fileNames, compilerOpts, host); 125 | if (this.options.translateBuiltins) { 126 | this.fc.initializeTypeBasedConversion(program.getTypeChecker(), compilerOpts, host); 127 | } 128 | 129 | // Only write files that were explicitly passed in. 130 | let fileSet: {[s: string]: boolean} = {}; 131 | fileNames.forEach((f) => fileSet[f] = true); 132 | this.errors = []; 133 | 134 | program.getSourceFiles() 135 | .filter((sourceFile) => fileSet[sourceFile.fileName]) 136 | // Do not generate output for .d.ts files. 137 | .filter((sourceFile: ts.SourceFile) => !sourceFile.fileName.match(/\.d\.ts$/)) 138 | .forEach((f: ts.SourceFile) => { 139 | let dartCode = this.translate(f); 140 | let outputFile = this.getOutputPath(f.fileName, destinationRoot); 141 | mkdirP(path.dirname(outputFile)); 142 | fs.writeFileSync(outputFile, dartCode); 143 | }); 144 | this.checkForErrors(program); 145 | } 146 | 147 | translateProgram(program: ts.Program, host: ts.CompilerHost): {[path: string]: string} { 148 | if (this.options.translateBuiltins) { 149 | this.fc.initializeTypeBasedConversion( 150 | program.getTypeChecker(), program.getCompilerOptions(), host); 151 | } 152 | let paths: {[path: string]: string} = {}; 153 | this.errors = []; 154 | program.getSourceFiles() 155 | .filter( 156 | (sourceFile: ts.SourceFile) => 157 | (!sourceFile.fileName.match(/\.d\.ts$/) && !!sourceFile.fileName.match(/\.[jt]s$/))) 158 | .forEach((f) => paths[f.fileName] = this.translate(f)); 159 | this.checkForErrors(program); 160 | return paths; 161 | } 162 | 163 | private getCompilerOptions() { 164 | let opts: ts.CompilerOptions = {}; 165 | for (let k of Object.keys(COMPILER_OPTIONS)) opts[k] = COMPILER_OPTIONS[k]; 166 | opts.rootDir = this.options.basePath; 167 | return opts; 168 | } 169 | 170 | private createCompilerHost(): ts.CompilerHost { 171 | let defaultLibFileName = ts.getDefaultLibFileName(COMPILER_OPTIONS); 172 | defaultLibFileName = this.normalizeSlashes(defaultLibFileName); 173 | let compilerHost: ts.CompilerHost = { 174 | getSourceFile: (sourceName, languageVersion) => { 175 | let sourcePath = sourceName; 176 | if (sourceName === defaultLibFileName) { 177 | sourcePath = ts.getDefaultLibFilePath(COMPILER_OPTIONS); 178 | } 179 | if (!fs.existsSync(sourcePath)) return undefined; 180 | let contents = fs.readFileSync(sourcePath, 'UTF-8'); 181 | return ts.createSourceFile(sourceName, contents, COMPILER_OPTIONS.target, true); 182 | }, 183 | writeFile(name, text, writeByteOrderMark) { fs.writeFile(name, text); }, 184 | fileExists: (filename) => fs.existsSync(filename), 185 | readFile: (filename) => fs.readFileSync(filename, 'utf-8'), 186 | getDefaultLibFileName: () => defaultLibFileName, 187 | useCaseSensitiveFileNames: () => true, 188 | getCanonicalFileName: (filename) => filename, 189 | getCurrentDirectory: () => '', 190 | getNewLine: () => '\n', 191 | }; 192 | compilerHost.resolveModuleNames = getModuleResolver(compilerHost); 193 | return compilerHost; 194 | } 195 | 196 | // Visible for testing. 197 | getOutputPath(filePath: string, destinationRoot: string): string { 198 | let relative = this.getRelativeFileName(filePath); 199 | let dartFile = relative.replace(/.(js|es6|ts)$/, '.dart'); 200 | return this.normalizeSlashes(path.join(destinationRoot, dartFile)); 201 | } 202 | 203 | private translate(sourceFile: ts.SourceFile): string { 204 | this.currentFile = sourceFile; 205 | this.output = new Output( 206 | sourceFile, this.getRelativeFileName(sourceFile.fileName), this.options.generateSourceMap); 207 | this.lastCommentIdx = -1; 208 | this.visit(sourceFile); 209 | let result = this.output.getResult(); 210 | return this.formatCode(result, sourceFile); 211 | } 212 | 213 | private formatCode(code: string, context: ts.Node) { 214 | let result = dartStyle.formatCode(code); 215 | if (result.error) { 216 | this.reportError(context, result.error); 217 | } 218 | return result.code; 219 | } 220 | 221 | private checkForErrors(program: ts.Program) { 222 | let errors = this.errors; 223 | 224 | let diagnostics = program.getGlobalDiagnostics().concat(program.getSyntacticDiagnostics()); 225 | 226 | if ((errors.length || diagnostics.length) && this.options.translateBuiltins) { 227 | // Only report semantic diagnostics if ts2dart failed; this code is not a generic compiler, so 228 | // only yields TS errors if they could be the cause of ts2dart issues. 229 | // This greatly speeds up tests and execution. 230 | diagnostics = diagnostics.concat(program.getSemanticDiagnostics()); 231 | } 232 | 233 | let diagnosticErrs = diagnostics.map((d) => this.diagnosticToString(d)); 234 | if (diagnosticErrs.length) errors = errors.concat(diagnosticErrs); 235 | 236 | if (errors.length) { 237 | let e = new Error(errors.join('\n')); 238 | e.name = 'TS2DartError'; 239 | throw e; 240 | } 241 | } 242 | 243 | private diagnosticToString(diagnostic: ts.Diagnostic): string { 244 | let msg = ''; 245 | if (diagnostic.file) { 246 | let pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 247 | let fn = this.getRelativeFileName(diagnostic.file.fileName); 248 | msg += ` ${fn}:${pos.line + 1}:${pos.character + 1}`; 249 | } 250 | msg += ': '; 251 | msg += ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 252 | return msg; 253 | } 254 | 255 | /** 256 | * Returns `filePath`, relativized to the program's `basePath`. 257 | * @param filePath path to relativize. 258 | */ 259 | getRelativeFileName(filePath: string) { 260 | let base = this.options.basePath || ''; 261 | if (filePath[0] === '/' && filePath.indexOf(base) !== 0 && !filePath.match(/\.d\.ts$/)) { 262 | throw new Error(`Files must be located under base, got ${filePath} vs ${base}`); 263 | } 264 | let rel = path.relative(base, filePath); 265 | if (rel.indexOf('../') === 0) { 266 | // filePath is outside of rel, just use it directly. 267 | rel = filePath; 268 | } 269 | return this.normalizeSlashes(rel); 270 | } 271 | 272 | emit(s: string) { this.output.emit(s); } 273 | emitNoSpace(s: string) { this.output.emitNoSpace(s); } 274 | 275 | reportError(n: ts.Node, message: string) { 276 | let file = n.getSourceFile() || this.currentFile; 277 | let fileName = this.getRelativeFileName(file.fileName); 278 | let start = n.getStart(file); 279 | let pos = file.getLineAndCharacterOfPosition(start); 280 | // Line and character are 0-based. 281 | let fullMessage = `${fileName}:${pos.line + 1}:${pos.character + 1}: ${message}`; 282 | if (this.options.failFast) throw new Error(fullMessage); 283 | this.errors.push(fullMessage); 284 | } 285 | 286 | visit(node: ts.Node) { 287 | this.output.addSourceMapping(node); 288 | try { 289 | let comments = ts.getLeadingCommentRanges(this.currentFile.text, node.getFullStart()); 290 | if (comments) { 291 | comments.forEach((c) => { 292 | if (c.pos <= this.lastCommentIdx) return; 293 | this.lastCommentIdx = c.pos; 294 | let text = this.currentFile.text.substring(c.pos, c.end); 295 | this.emitNoSpace('\n'); 296 | this.emit(this.translateComment(text)); 297 | if (c.hasTrailingNewLine) this.emitNoSpace('\n'); 298 | }); 299 | } 300 | 301 | for (let i = 0; i < this.transpilers.length; i++) { 302 | if (this.transpilers[i].visitNode(node)) return; 303 | } 304 | this.reportError( 305 | node, `Unsupported node type ${(ts).SyntaxKind[node.kind]}: ${node.getFullText()}`); 306 | } catch (e) { 307 | this.reportError(node, 'ts2dart crashed ' + e.stack); 308 | } 309 | } 310 | 311 | private normalizeSlashes(path: string) { return path.replace(/\\/g, '/'); } 312 | 313 | private translateComment(comment: string): string { 314 | comment = comment.replace(/\{@link ([^\}]+)\}/g, '[$1]'); 315 | 316 | // Remove the following tags and following comments till end of line. 317 | comment = comment.replace(/@param.*$/gm, ''); 318 | comment = comment.replace(/@throws.*$/gm, ''); 319 | comment = comment.replace(/@return.*$/gm, ''); 320 | 321 | // Remove the following tags. 322 | comment = comment.replace(/@module/g, ''); 323 | comment = comment.replace(/@description/g, ''); 324 | comment = comment.replace(/@deprecated/g, ''); 325 | 326 | return comment; 327 | } 328 | } 329 | 330 | export function getModuleResolver(compilerHost: ts.CompilerHost) { 331 | return (moduleNames: string[], containingFile: string): ts.ResolvedModule[] => { 332 | let res: ts.ResolvedModule[] = []; 333 | for (let mod of moduleNames) { 334 | let lookupRes = 335 | ts.nodeModuleNameResolver(mod, containingFile, COMPILER_OPTIONS, compilerHost); 336 | if (lookupRes.resolvedModule) { 337 | res.push(lookupRes.resolvedModule); 338 | continue; 339 | } 340 | lookupRes = ts.classicNameResolver(mod, containingFile, COMPILER_OPTIONS, compilerHost); 341 | if (lookupRes.resolvedModule) { 342 | res.push(lookupRes.resolvedModule); 343 | continue; 344 | } 345 | res.push(undefined); 346 | } 347 | return res; 348 | }; 349 | } 350 | 351 | class Output { 352 | private result: string = ''; 353 | private column: number = 1; 354 | private line: number = 1; 355 | 356 | // Position information. 357 | private generateSourceMap: boolean; 358 | private sourceMap: SourceMapGenerator; 359 | 360 | constructor( 361 | private currentFile: ts.SourceFile, private relativeFileName: string, 362 | generateSourceMap: boolean) { 363 | if (generateSourceMap) { 364 | this.sourceMap = new SourceMapGenerator({file: relativeFileName + '.dart'}); 365 | this.sourceMap.setSourceContent(relativeFileName, this.currentFile.text); 366 | } 367 | } 368 | 369 | emit(str: string) { 370 | this.emitNoSpace(' '); 371 | this.emitNoSpace(str); 372 | } 373 | 374 | emitNoSpace(str: string) { 375 | this.result += str; 376 | for (let i = 0; i < str.length; i++) { 377 | if (str[i] === '\n') { 378 | this.line++; 379 | this.column = 0; 380 | } else { 381 | this.column++; 382 | } 383 | } 384 | } 385 | 386 | getResult(): string { return this.result + this.generateSourceMapComment(); } 387 | 388 | addSourceMapping(n: ts.Node) { 389 | if (!this.generateSourceMap) return; // source maps disabled. 390 | let file = n.getSourceFile() || this.currentFile; 391 | let start = n.getStart(file); 392 | let pos = file.getLineAndCharacterOfPosition(start); 393 | 394 | let mapping: SourceMap.Mapping = { 395 | original: {line: pos.line + 1, column: pos.character}, 396 | generated: {line: this.line, column: this.column}, 397 | source: this.relativeFileName, 398 | }; 399 | 400 | this.sourceMap.addMapping(mapping); 401 | } 402 | 403 | private generateSourceMapComment() { 404 | if (!this.sourceMap) return ''; 405 | let base64map = new Buffer(JSON.stringify(this.sourceMap)).toString('base64'); 406 | return '\n\n//# sourceMappingURL=data:application/json;base64,' + base64map; 407 | } 408 | } 409 | 410 | function showHelp() { 411 | console.log(` 412 | Usage: ts2dart [input-files] [arguments] 413 | 414 | --help show this dialog 415 | 416 | --failFast Fail on the first error, do not collect multiple. Allows easier debugging 417 | as stack traces lead directly to the offending line 418 | 419 | --generateLibraryName Whether to generate 'library a.b.c;' names from relative file paths. 420 | 421 | --generateSourceMap Whether to generate source maps. 422 | 423 | --tsconfig A tsconfig.json to use to configure TypeScript compilation. 424 | 425 | --basePath A base path to relativize absolute file paths against. This 426 | is useful for library name generation (see above) and nicer 427 | file names in error messages. 428 | 429 | --translateBuiltins Translate calls to builtins, i.e. seemlessly convert from \` Array\` to \` List\`, 430 | and convert the corresponding methods. Requires type checking. 431 | 432 | --enforceUnderscoreConventions Enforce conventions of public/private keyword and underscore prefix 433 | `); 434 | process.exit(0); 435 | } 436 | 437 | // CLI entry point 438 | if (require.main === module) { 439 | let args = require('minimist')(process.argv.slice(2), {base: 'string'}); 440 | if (args.help) showHelp(); 441 | try { 442 | let transpiler = new Transpiler(args); 443 | console.error('Transpiling', args._, 'to', args.destination); 444 | transpiler.transpile(args._, args.destination); 445 | } catch (e) { 446 | if (e.name !== 'TS2DartError') throw e; 447 | console.error(e.message); 448 | process.exit(1); 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /lib/mkdirp.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export default function mkdirP(p: string) { 5 | // Convert input path to absolute and then relative so that we always have relative path in the 6 | // end. This can be made simpler when path.isAbsolute is available in node v0.12. 7 | p = path.resolve(p); 8 | p = path.relative('', p); 9 | 10 | let pathToCreate = ''; 11 | p.split(path.sep).forEach(dirName => { 12 | pathToCreate = path.join(pathToCreate, dirName); 13 | if (!fs.existsSync(pathToCreate)) { 14 | fs.mkdirSync(pathToCreate); 15 | } 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /lib/module.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as base from './base'; 4 | import {FacadeConverter} from './facade_converter'; 5 | import {Transpiler} from './main'; 6 | 7 | export default class ModuleTranspiler extends base.TranspilerBase { 8 | constructor(tr: Transpiler, private fc: FacadeConverter, private generateLibraryName: boolean) { 9 | super(tr); 10 | } 11 | 12 | visitNode(node: ts.Node): boolean { 13 | switch (node.kind) { 14 | case ts.SyntaxKind.SourceFile: 15 | let sf = node; 16 | if (this.generateLibraryName) { 17 | this.emit('library'); 18 | this.emit(this.getLibraryName(sf.fileName)); 19 | this.emit(';'); 20 | } 21 | this.fc.emitExtraImports(sf); 22 | ts.forEachChild(sf, this.visit.bind(this)); 23 | break; 24 | case ts.SyntaxKind.EndOfFileToken: 25 | ts.forEachChild(node, this.visit.bind(this)); 26 | break; 27 | case ts.SyntaxKind.ImportDeclaration: 28 | let importDecl = node; 29 | if (importDecl.importClause) { 30 | if (this.isEmptyImport(importDecl)) return true; 31 | this.emit('import'); 32 | this.visitExternalModuleReferenceExpr(importDecl.moduleSpecifier); 33 | this.visit(importDecl.importClause); 34 | } else { 35 | this.reportError(importDecl, 'bare import is unsupported'); 36 | } 37 | this.emit(';'); 38 | break; 39 | case ts.SyntaxKind.ImportClause: 40 | let importClause = node; 41 | if (importClause.name) this.fc.visitTypeName(importClause.name); 42 | if (importClause.namedBindings) { 43 | this.visit(importClause.namedBindings); 44 | } 45 | break; 46 | case ts.SyntaxKind.NamespaceImport: 47 | let nsImport = node; 48 | this.emit('as'); 49 | this.fc.visitTypeName(nsImport.name); 50 | break; 51 | case ts.SyntaxKind.NamedImports: 52 | this.emit('show'); 53 | let used = this.filterImports((node).elements); 54 | if (used.length === 0) { 55 | this.reportError(node, 'internal error, used imports must not be empty'); 56 | } 57 | this.visitList(used); 58 | break; 59 | case ts.SyntaxKind.NamedExports: 60 | let exportElements = (node).elements; 61 | this.emit('show'); 62 | if (exportElements.length === 0) this.reportError(node, 'empty export list'); 63 | this.visitList((node).elements); 64 | break; 65 | case ts.SyntaxKind.ImportSpecifier: 66 | case ts.SyntaxKind.ExportSpecifier: 67 | let spec = node; 68 | if (spec.propertyName) { 69 | this.reportError(spec.propertyName, 'import/export renames are unsupported in Dart'); 70 | } 71 | this.fc.visitTypeName(spec.name); 72 | break; 73 | case ts.SyntaxKind.ExportDeclaration: 74 | let exportDecl = node; 75 | this.emit('export'); 76 | if (exportDecl.moduleSpecifier) { 77 | this.visitExternalModuleReferenceExpr(exportDecl.moduleSpecifier); 78 | } else { 79 | this.reportError(node, 're-exports must have a module URL (export x from "./y").'); 80 | } 81 | if (exportDecl.exportClause) this.visit(exportDecl.exportClause); 82 | this.emit(';'); 83 | break; 84 | case ts.SyntaxKind.ImportEqualsDeclaration: 85 | let importEqDecl = node; 86 | this.emit('import'); 87 | this.visit(importEqDecl.moduleReference); 88 | this.emit('as'); 89 | this.fc.visitTypeName(importEqDecl.name); 90 | this.emit(';'); 91 | break; 92 | case ts.SyntaxKind.ExternalModuleReference: 93 | this.visitExternalModuleReferenceExpr((node).expression); 94 | break; 95 | 96 | default: 97 | return false; 98 | } 99 | return true; 100 | } 101 | 102 | private static isIgnoredImport(e: ts.ImportSpecifier) { 103 | // TODO: unify with facade_converter.ts 104 | let name = base.ident(e.name); 105 | switch (name) { 106 | case 'CONST': 107 | case 'CONST_EXPR': 108 | case 'normalizeBlank': 109 | case 'forwardRef': 110 | case 'ABSTRACT': 111 | case 'IMPLEMENTS': 112 | return true; 113 | default: 114 | return false; 115 | } 116 | } 117 | 118 | private visitExternalModuleReferenceExpr(expr: ts.Expression) { 119 | // TODO: what if this isn't a string literal? 120 | let moduleName = expr; 121 | let text = moduleName.text; 122 | if (text.match(/^\.\//)) { 123 | // Strip './' to be more Dart-idiomatic. 124 | text = text.substring(2); 125 | } else if (!text.match(/^\.\.\//)) { 126 | // Replace '@angular' with 'angular2' for Dart. 127 | text = text.replace(/^@angular\//, 'angular2/'); 128 | // Unprefixed/absolute imports are package imports. 129 | text = 'package:' + text; 130 | } 131 | this.emit(JSON.stringify(text + '.dart')); 132 | } 133 | 134 | private isEmptyImport(n: ts.ImportDeclaration): boolean { 135 | let bindings = n.importClause.namedBindings; 136 | if (bindings.kind !== ts.SyntaxKind.NamedImports) return false; 137 | let elements = (bindings).elements; 138 | // An import list being empty *after* filtering is ok, but if it's empty in the code itself, 139 | // it's nonsensical code, so probably a programming error. 140 | if (elements.length === 0) this.reportError(n, 'empty import list'); 141 | return elements.every(ModuleTranspiler.isIgnoredImport); 142 | } 143 | 144 | private filterImports(ns: ts.ImportOrExportSpecifier[]) { 145 | return ns.filter((e) => !ModuleTranspiler.isIgnoredImport(e)); 146 | } 147 | 148 | // For the Dart keyword list see 149 | // https://www.dartlang.org/docs/dart-up-and-running/ch02.html#keywords 150 | private static DART_RESERVED_WORDS = 151 | ('assert break case catch class const continue default do else enum extends false final ' + 152 | 'finally for if in is new null rethrow return super switch this throw true try let void ' + 153 | 'while with') 154 | .split(/ /); 155 | 156 | getLibraryName(fileName: string) { 157 | fileName = this.getRelativeFileName(fileName); 158 | let parts = fileName.split('/'); 159 | return parts.filter((p) => p.length > 0) 160 | .map((p) => p.replace(/^@/, '')) 161 | .map((p) => p.replace(/[^\w.]/g, '_')) 162 | .map((p) => p.replace(/\.[jt]s$/g, '')) 163 | .map((p) => ModuleTranspiler.DART_RESERVED_WORDS.indexOf(p) !== -1 ? '_' + p : p) 164 | .join('.'); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/statement.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as base from './base'; 3 | import {Transpiler} from './main'; 4 | 5 | type ClassLike = ts.ClassDeclaration | ts.InterfaceDeclaration; 6 | 7 | export default class StatementTranspiler extends base.TranspilerBase { 8 | constructor(tr: Transpiler) { super(tr); } 9 | 10 | visitNode(node: ts.Node): boolean { 11 | switch (node.kind) { 12 | case ts.SyntaxKind.EmptyStatement: 13 | this.emit(';'); 14 | break; 15 | case ts.SyntaxKind.ReturnStatement: 16 | let retStmt = node; 17 | this.emit('return'); 18 | if (retStmt.expression) this.visit(retStmt.expression); 19 | this.emit(';'); 20 | break; 21 | case ts.SyntaxKind.BreakStatement: 22 | case ts.SyntaxKind.ContinueStatement: 23 | let breakContinue = node; 24 | this.emit(breakContinue.kind === ts.SyntaxKind.BreakStatement ? 'break' : 'continue'); 25 | if (breakContinue.label) this.visit(breakContinue.label); 26 | this.emit(';'); 27 | break; 28 | case ts.SyntaxKind.VariableStatement: 29 | let variableStmt = node; 30 | this.visit(variableStmt.declarationList); 31 | this.emit(';'); 32 | break; 33 | case ts.SyntaxKind.ExpressionStatement: 34 | let expr = node; 35 | this.visit(expr.expression); 36 | this.emit(';'); 37 | break; 38 | case ts.SyntaxKind.SwitchStatement: 39 | let switchStmt = node; 40 | this.emit('switch ('); 41 | this.visit(switchStmt.expression); 42 | this.emit(')'); 43 | this.visit(switchStmt.caseBlock); 44 | break; 45 | case ts.SyntaxKind.CaseBlock: 46 | this.emit('{'); 47 | this.visitEach((node).clauses); 48 | this.emit('}'); 49 | break; 50 | case ts.SyntaxKind.CaseClause: 51 | let caseClause = node; 52 | this.emit('case'); 53 | this.visit(caseClause.expression); 54 | this.emit(':'); 55 | this.visitEach(caseClause.statements); 56 | break; 57 | case ts.SyntaxKind.DefaultClause: 58 | this.emit('default :'); 59 | this.visitEach((node).statements); 60 | break; 61 | case ts.SyntaxKind.IfStatement: 62 | let ifStmt = node; 63 | this.emit('if ('); 64 | this.visit(ifStmt.expression); 65 | this.emit(')'); 66 | this.visit(ifStmt.thenStatement); 67 | if (ifStmt.elseStatement) { 68 | this.emit('else'); 69 | this.visit(ifStmt.elseStatement); 70 | } 71 | break; 72 | case ts.SyntaxKind.ForStatement: 73 | let forStmt = node; 74 | this.emit('for ('); 75 | if (forStmt.initializer) this.visit(forStmt.initializer); 76 | this.emit(';'); 77 | if (forStmt.condition) this.visit(forStmt.condition); 78 | this.emit(';'); 79 | if (forStmt.incrementor) this.visit(forStmt.incrementor); 80 | this.emit(')'); 81 | this.visit(forStmt.statement); 82 | break; 83 | case ts.SyntaxKind.ForInStatement: 84 | // TODO(martinprobst): Dart's for-in loops actually have different semantics, they are more 85 | // like for-of loops, iterating over collections. 86 | let forInStmt = node; 87 | this.emit('for ('); 88 | if (forInStmt.initializer) this.visit(forInStmt.initializer); 89 | this.emit('in'); 90 | this.visit(forInStmt.expression); 91 | this.emit(')'); 92 | this.visit(forInStmt.statement); 93 | break; 94 | case ts.SyntaxKind.ForOfStatement: 95 | let forOfStmt = node; 96 | this.emit('for ('); 97 | if (forOfStmt.initializer) this.visit(forOfStmt.initializer); 98 | this.emit('in'); 99 | this.visit(forOfStmt.expression); 100 | this.emit(')'); 101 | this.visit(forOfStmt.statement); 102 | break; 103 | case ts.SyntaxKind.WhileStatement: 104 | let whileStmt = node; 105 | this.emit('while ('); 106 | this.visit(whileStmt.expression); 107 | this.emit(')'); 108 | this.visit(whileStmt.statement); 109 | break; 110 | case ts.SyntaxKind.DoStatement: 111 | let doStmt = node; 112 | this.emit('do'); 113 | this.visit(doStmt.statement); 114 | this.emit('while ('); 115 | this.visit(doStmt.expression); 116 | this.emit(') ;'); 117 | break; 118 | 119 | case ts.SyntaxKind.ThrowStatement: 120 | let throwStmt = node; 121 | let surroundingCatchClause = this.getAncestor(throwStmt, ts.SyntaxKind.CatchClause); 122 | if (surroundingCatchClause) { 123 | let ref = (surroundingCatchClause).variableDeclaration; 124 | if (ref.getText() === throwStmt.expression.getText()) { 125 | this.emit('rethrow'); 126 | this.emit(';'); 127 | break; 128 | } 129 | } 130 | 131 | this.emit('throw'); 132 | this.visit(throwStmt.expression); 133 | this.emit(';'); 134 | break; 135 | case ts.SyntaxKind.TryStatement: 136 | let tryStmt = node; 137 | this.emit('try'); 138 | this.visit(tryStmt.tryBlock); 139 | if (tryStmt.catchClause) { 140 | this.visit(tryStmt.catchClause); 141 | } 142 | if (tryStmt.finallyBlock) { 143 | this.emit('finally'); 144 | this.visit(tryStmt.finallyBlock); 145 | } 146 | break; 147 | case ts.SyntaxKind.CatchClause: 148 | let ctch = node; 149 | if (ctch.variableDeclaration.type) { 150 | this.emit('on'); 151 | this.visit(ctch.variableDeclaration.type); 152 | } 153 | this.emit('catch'); 154 | this.emit('('); 155 | this.visit(ctch.variableDeclaration.name); 156 | this.emit(','); 157 | this.visit(ctch.variableDeclaration.name); 158 | this.emitNoSpace('_stack'); 159 | this.emit(')'); 160 | this.visit(ctch.block); 161 | break; 162 | 163 | case ts.SyntaxKind.Block: 164 | this.emit('{'); 165 | this.visitEach((node).statements); 166 | this.emit('}'); 167 | break; 168 | default: 169 | return false; 170 | } 171 | return true; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/type.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | import * as base from './base'; 4 | import {FacadeConverter} from './facade_converter'; 5 | import {Transpiler} from './main'; 6 | 7 | export default class TypeTranspiler extends base.TranspilerBase { 8 | constructor(tr: Transpiler, private fc: FacadeConverter) { super(tr); } 9 | 10 | visitNode(node: ts.Node): boolean { 11 | switch (node.kind) { 12 | case ts.SyntaxKind.TypeLiteral: 13 | let indexType = this.maybeDestructureIndexType(node); 14 | if (indexType) { 15 | // This is effectively a Map. 16 | this.emit('Map <'); 17 | this.visit(indexType[0]); 18 | this.emit(','); 19 | this.visit(indexType[1]); 20 | this.emit('>'); 21 | } else { 22 | // Dart doesn't support other type literals. 23 | this.emit('dynamic'); 24 | } 25 | break; 26 | case ts.SyntaxKind.UnionType: 27 | this.emit('dynamic /*'); 28 | this.visitList((node).types, '|'); 29 | this.emit('*/'); 30 | break; 31 | case ts.SyntaxKind.TypeReference: 32 | let typeRef = node; 33 | this.fc.visitTypeName(typeRef.typeName); 34 | this.maybeVisitTypeArguments(typeRef); 35 | break; 36 | case ts.SyntaxKind.TypeAssertionExpression: 37 | let typeAssertExpr = node; 38 | if (this.isReifiedTypeLiteral(typeAssertExpr)) { 39 | this.visit(typeAssertExpr.expression); 40 | break; // type is handled by the container literal itself. 41 | } 42 | this.emit('('); 43 | this.visit(typeAssertExpr.expression); 44 | this.emit('as'); 45 | this.visit(typeAssertExpr.type); 46 | this.emit(')'); 47 | break; 48 | case ts.SyntaxKind.TypeParameter: 49 | let typeParam = node; 50 | this.visit(typeParam.name); 51 | if (typeParam.constraint) { 52 | this.emit('extends'); 53 | this.visit(typeParam.constraint); 54 | } 55 | break; 56 | case ts.SyntaxKind.ArrayType: 57 | this.emit('List'); 58 | this.emit('<'); 59 | this.visit((node).elementType); 60 | this.emit('>'); 61 | break; 62 | case ts.SyntaxKind.FunctionType: 63 | this.emit('dynamic /*'); 64 | this.emit(node.getText()); 65 | this.emit('*/'); 66 | break; 67 | case ts.SyntaxKind.QualifiedName: 68 | let first = node; 69 | this.visit(first.left); 70 | this.emit('.'); 71 | this.visit(first.right); 72 | break; 73 | case ts.SyntaxKind.Identifier: 74 | let ident = node; 75 | this.fc.visitTypeName(ident); 76 | break; 77 | case ts.SyntaxKind.NumberKeyword: 78 | this.emit('num'); 79 | break; 80 | case ts.SyntaxKind.StringKeyword: 81 | this.emit('String'); 82 | break; 83 | case ts.SyntaxKind.VoidKeyword: 84 | this.emit('void'); 85 | break; 86 | case ts.SyntaxKind.BooleanKeyword: 87 | this.emit('bool'); 88 | break; 89 | case ts.SyntaxKind.AnyKeyword: 90 | this.emit('dynamic'); 91 | break; 92 | default: 93 | return false; 94 | } 95 | return true; 96 | } 97 | 98 | isReifiedTypeLiteral(node: ts.TypeAssertion): boolean { 99 | if (node.expression.kind === ts.SyntaxKind.ArrayLiteralExpression && 100 | node.type.kind === ts.SyntaxKind.ArrayType) { 101 | return true; 102 | } else if ( 103 | node.expression.kind === ts.SyntaxKind.ObjectLiteralExpression && 104 | node.type.kind === ts.SyntaxKind.TypeLiteral) { 105 | return true; 106 | } 107 | return false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts2dart", 3 | "version": "0.9.11", 4 | "description": "Transpile TypeScript code to Dart", 5 | "main": "build/lib/main.js", 6 | "bin": "build/lib/main.js", 7 | "typings": "build/definitions/main.d.ts", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "dependencies": { 12 | "dart-style": "^0.2.7", 13 | "minimist": "^1.1.1", 14 | "source-map": "^0.4.2", 15 | "source-map-support": "^0.3.1", 16 | "typescript": "v1.9.0-dev.20160409" 17 | }, 18 | "devDependencies": { 19 | "chai": "^2.1.1", 20 | "clang-format": "^1.0.37", 21 | "fs-extra": "^0.18.0", 22 | "gulp": "^3.8.11", 23 | "gulp-clang-format": "^1.0.21", 24 | "gulp-mocha": "^2.0.0", 25 | "gulp-sourcemaps": "^1.5.0", 26 | "gulp-tslint": "^4.3.4", 27 | "gulp-typescript": "^2.7.6", 28 | "gulp-util": "^3.0.4", 29 | "merge2": "^0.3.1", 30 | "npm-release": "^1.0.0", 31 | "temp": "^0.8.1", 32 | "tsd": "^0.6.0", 33 | "tslint": "^3.7.1", 34 | "which": "^1.0.9" 35 | }, 36 | "scripts": { 37 | "prepublish": "npm install tsd@^0.6.0 && tsd reinstall --overwrite && gulp compile", 38 | "test": "gulp test", 39 | "release": "npm-release" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/angular/ts2dart.git" 44 | }, 45 | "keywords": [ 46 | "typescript", 47 | "dart" 48 | ], 49 | "contributors": [ 50 | "Alex Eagle (https://angularjs.org/)", 51 | "Martin Probst (https://angularjs.org/)" 52 | ], 53 | "license": "Apache-2.0", 54 | "bugs": { 55 | "url": "https://github.com/angular/ts2dart/issues" 56 | }, 57 | "homepage": "https://github.com/angular/ts2dart" 58 | } 59 | -------------------------------------------------------------------------------- /test/call_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectErroneousCode, expectTranslate} from './test_support'; 3 | 4 | describe('calls', () => { 5 | it('translates destructuring parameters', () => { 6 | expectTranslate('function x({p = null, d = false} = {}) {}') 7 | .to.equal('x({p: null, d: false}) {}'); 8 | expectErroneousCode('function x({a=false}={a:true})') 9 | .to.throw('cannot have both an inner and outer initializer'); 10 | expectErroneousCode('function x({a=false}=true)') 11 | .to.throw('initializers for named parameters must be object literals'); 12 | expectTranslate('class X { constructor() { super({p: 1}); } }').to.equal(`class X { 13 | X() : super(p: 1) { 14 | /* super call moved to initializer */; 15 | } 16 | }`); 17 | }); 18 | it('hacks last object literal parameters into named parameter', () => { 19 | expectTranslate('f(x, {a: 12, b: 4});').to.equal('f(x, a: 12, b: 4);'); 20 | expectTranslate('f({a: 12});').to.equal('f(a: 12);'); 21 | expectTranslate('f({"a": 12});').to.equal('f({"a": 12});'); 22 | expectTranslate('new X(x, {a: 12, b: 4});').to.equal('new X(x, a: 12, b: 4);'); 23 | expectTranslate('f(x, {});').to.equal('f(x, {});'); 24 | }); 25 | it('translates calls', () => { 26 | expectTranslate('foo();').to.equal('foo();'); 27 | expectTranslate('foo(1, 2);').to.equal('foo(1, 2);'); 28 | }); 29 | it('translates new calls', () => { 30 | expectTranslate('new Foo();').to.equal('new Foo();'); 31 | expectTranslate('new Foo(1, 2);').to.equal('new Foo(1, 2);'); 32 | expectTranslate('new Foo(1, 2);').to.equal('new Foo(1, 2);'); 33 | }); 34 | it('supports generic type parameters', 35 | () => { expectTranslate('var s = foo();').to.equal('var s = foo/*< String >*/();'); }); 36 | it('translates "super()" constructor calls', () => { 37 | expectTranslate('class X { constructor() { super(1); } }').to.equal(`class X { 38 | X() : super(1) { 39 | /* super call moved to initializer */; 40 | } 41 | }`); 42 | expectErroneousCode('class X { constructor() { if (y) super(1, 2); } }') 43 | .to.throw('super calls must be immediate children of their constructors'); 44 | expectTranslate('class X { constructor() { a(); super(1); b(); } }').to.equal(`class X { 45 | X() : super(1) { 46 | a(); 47 | /* super call moved to initializer */ 48 | ; 49 | b(); 50 | } 51 | }`); 52 | }); 53 | it('translates "super.x()" super method calls', () => { 54 | expectTranslate('class X { y() { super.z(1); } }').to.equal(`class X { 55 | y() { 56 | super.z(1); 57 | } 58 | }`); 59 | }); 60 | it('transpiles new calls without arguments', 61 | () => { expectTranslate('new Foo;').to.equal('new Foo();'); }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/declaration_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectErroneousCode, expectTranslate} from './test_support'; 3 | 4 | describe('variables', () => { 5 | it('should print variable declaration with initializer', 6 | () => { expectTranslate('var a:number = 1;').to.equal('num a = 1;'); }); 7 | it('should print variable declaration', () => { 8 | expectTranslate('var a:number;').to.equal('num a;'); 9 | expectTranslate('var a;').to.equal('var a;'); 10 | expectTranslate('var a:any;').to.equal('dynamic a;'); 11 | }); 12 | it('should transpile variable declaration lists', () => { 13 | expectTranslate('var a: A;').to.equal('A a;'); 14 | expectTranslate('var a, b;').to.equal('var a, b;'); 15 | }); 16 | it('should transpile variable declaration lists with initializers', () => { 17 | expectTranslate('var a = 0;').to.equal('var a = 0;'); 18 | expectTranslate('var a, b = 0;').to.equal('var a, b = 0;'); 19 | expectTranslate('var a = 1, b = 0;').to.equal('var a = 1, b = 0;'); 20 | }); 21 | it('does not support vardecls containing more than one type (implicit or explicit)', () => { 22 | let msg = 'Variables in a declaration list of more than one variable cannot by typed'; 23 | expectErroneousCode('var a: A, untyped;').to.throw(msg); 24 | expectErroneousCode('var untyped, b: B;').to.throw(msg); 25 | expectErroneousCode('var n: number, s: string;').to.throw(msg); 26 | expectErroneousCode('var untyped, n: number, s: string;').to.throw(msg); 27 | }); 28 | 29 | it('supports const', () => { 30 | // NB: const X = CONST_EXPR(1); is translated as deep-const, see tests in facade_converter_test. 31 | // Arbitrary expressions translate const ==> final... 32 | expectTranslate('const A = 1 + 2;').to.equal('final A = 1 + 2;'); 33 | // ... but literals are special cased to be deep const. 34 | expectTranslate('const A = 1, B = 2;').to.equal('const A = 1, B = 2;'); 35 | expectTranslate('const A: number = 1;').to.equal('const num A = 1;'); 36 | }); 37 | }); 38 | 39 | describe('classes', () => { 40 | it('should translate classes', () => { expectTranslate('class X {}').to.equal('class X {}'); }); 41 | it('should support extends', 42 | () => { expectTranslate('class X extends Y {}').to.equal('class X extends Y {}'); }); 43 | it('should support implements', () => { 44 | expectTranslate('class X implements Y, Z {}').to.equal('class X implements Y, Z {}'); 45 | }); 46 | it('should support implements', () => { 47 | expectTranslate('class X extends Y implements Z {}') 48 | .to.equal('class X extends Y implements Z {}'); 49 | }); 50 | it('should support abstract', 51 | () => { expectTranslate('abstract class X {}').to.equal('abstract class X {}'); }); 52 | 53 | describe('members', () => { 54 | it('supports empty declarations', 55 | () => { expectTranslate('class X { ; }').to.equal('class X {}'); }); 56 | it('supports fields', () => { 57 | expectTranslate('class X { x: number; y: string; }').to.equal(`class X { 58 | num x; 59 | String y; 60 | }`); 61 | expectTranslate('class X { x; }').to.equal(`class X { 62 | var x; 63 | }`); 64 | }); 65 | it('supports function typed fields', () => { 66 | expectTranslate( 67 | 'interface FnDef {(y: number): string;}\n' + 68 | 'class X { x: FnDef; }') 69 | .to.equal(`typedef String FnDef(num y); 70 | 71 | class X { 72 | FnDef x; 73 | }`); 74 | }); 75 | it('supports field initializers', () => { 76 | expectTranslate('class X { x: number = 42; }').to.equal(`class X { 77 | num x = 42; 78 | }`); 79 | }); 80 | // TODO(martinprobst): Re-enable once Angular is migrated to TS. 81 | it('supports visibility modifiers', () => { 82 | expectTranslate('class X { private _x; x; }').to.equal(`class X { 83 | var _x; 84 | var x; 85 | }`); 86 | expectErroneousCode('class X { private x; }') 87 | .to.throw('private members must be prefixed with "_"'); 88 | expectErroneousCode('class X { constructor (private x) {} }') 89 | .to.throw('private members must be prefixed with "_"'); 90 | expectErroneousCode('class X { _x; }') 91 | .to.throw('public members must not be prefixed with "_"'); 92 | }); 93 | it('does not support protected', () => { 94 | expectErroneousCode('class X { protected x; }') 95 | .to.throw('protected declarations are unsupported'); 96 | }); 97 | it('supports static fields', () => { 98 | expectTranslate('class X { static x: number = 42; }').to.equal(`class X { 99 | static num x = 42; 100 | }`); 101 | }); 102 | it('supports methods', () => { 103 | expectTranslate('class X { x() { return 42; } }').to.equal(`class X { 104 | x() { 105 | return 42; 106 | } 107 | }`); 108 | }); 109 | it('supports abstract methods', () => { 110 | expectTranslate('abstract class X { abstract x(); }').to.equal(`abstract class X { 111 | x(); 112 | }`); 113 | }); 114 | it('supports method return types', () => { 115 | expectTranslate('class X { x(): number { return 42; } }').to.equal(`class X { 116 | num x() { 117 | return 42; 118 | } 119 | }`); 120 | }); 121 | it('supports method params', () => { 122 | expectTranslate('class X { x(a, b) { return 42; } }').to.equal(`class X { 123 | x(a, b) { 124 | return 42; 125 | } 126 | }`); 127 | }); 128 | it('supports method return types', () => { 129 | expectTranslate('class X { x( a : number, b : string ) { return 42; } }').to.equal(`class X { 130 | x(num a, String b) { 131 | return 42; 132 | } 133 | }`); 134 | }); 135 | it('supports get methods', () => { 136 | expectTranslate('class X { get y(): number {} }').to.equal(`class X { 137 | num get y {} 138 | }`); 139 | expectTranslate('class X { static get Y(): number {} }').to.equal(`class X { 140 | static num get Y {} 141 | }`); 142 | }); 143 | it('supports set methods', () => { 144 | expectTranslate('class X { set y(n: number) {} }').to.equal(`class X { 145 | set y(num n) {} 146 | }`); 147 | expectTranslate('class X { static get Y(): number {} }').to.equal(`class X { 148 | static num get Y {} 149 | }`); 150 | }); 151 | it('supports constructors', () => { 152 | expectTranslate('class X { constructor() {} }').to.equal(`class X { 153 | X() {} 154 | }`); 155 | }); 156 | it('supports parameter properties', () => { 157 | expectTranslate( 158 | 'class X { c: number; \n' + 159 | ' constructor(private _bar: B, ' + 160 | 'public foo: string = "hello", ' + 161 | 'private _goggles: boolean = true) {} }') 162 | .to.equal(`class X { 163 | B _bar; 164 | String foo; 165 | bool _goggles; 166 | num c; 167 | X(this._bar, [this.foo = "hello", this._goggles = true]) {} 168 | }`); 169 | expectTranslate( 170 | '@CONST class X { ' + 171 | 'constructor(public foo: string, b: number, private _marbles: boolean = true) {} }') 172 | .to.equal(`class X { 173 | final String foo; 174 | final bool _marbles; 175 | const X(this.foo, num b, [this._marbles = true]); 176 | }`); 177 | expectTranslate(`/* @ts2dart_const */ class X { 178 | constructor(public foo: string) {} 179 | foo() { return new Bar(); } 180 | }`).to.equal(`/* @ts2dart_const */ 181 | class X { 182 | final String foo; 183 | const X(this.foo); 184 | foo() { 185 | return new Bar(); 186 | } 187 | }`); 188 | }); 189 | }); 190 | }); 191 | 192 | describe('interfaces', () => { 193 | it('translates interfaces to abstract classes', 194 | () => { expectTranslate('interface X {}').to.equal('abstract class X {}'); }); 195 | it('translates interface extends to class implements', () => { 196 | expectTranslate('interface X extends Y, Z {}').to.equal('abstract class X implements Y, Z {}'); 197 | }); 198 | it('supports abstract methods', () => { 199 | expectTranslate('interface X { x(); }').to.equal(`abstract class X { 200 | x(); 201 | }`); 202 | }); 203 | it('supports interface properties', () => { 204 | expectTranslate('interface X { x: string; y; }').to.equal(`abstract class X { 205 | String x; 206 | var y; 207 | }`); 208 | }); 209 | }); 210 | 211 | describe('single call signature interfaces', () => { 212 | it('should support declaration', () => { 213 | expectTranslate('interface F { (n: number): boolean; }').to.equal('typedef bool F(num n);'); 214 | }); 215 | it('should support generics', () => { 216 | expectTranslate('interface F { (a: A): B; }').to.equal('typedef B F(A a);'); 217 | }); 218 | }); 219 | 220 | describe('enums', () => { 221 | it('should support basic enum declaration', () => { 222 | expectTranslate('enum Color { Red, Green, Blue }').to.equal('enum Color { Red, Green, Blue }'); 223 | }); 224 | it('does not support empty enum', 225 | () => { expectErroneousCode('enum Empty {}').to.throw('empty enums are not supported'); }); 226 | it('does not support enum with initializer', () => { 227 | expectErroneousCode('enum Color { Red = 1, Green, Blue = 4 }') 228 | .to.throw('enum initializers are not supported'); 229 | }); 230 | it('should support switch over enum', () => { 231 | expectTranslate('switch(c) { case Color.Red: break; default: break; }').to.equal(`switch (c) { 232 | case Color.Red: 233 | break; 234 | default: 235 | break; 236 | }`); 237 | }); 238 | it('does not support const enum', () => { 239 | expectErroneousCode('const enum Color { Red }').to.throw('const enums are not supported'); 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /test/decorator_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectErroneousCode, expectTranslate} from './test_support'; 3 | 4 | describe('decorators', () => { 5 | it('translates plain decorators', () => { 6 | expectTranslate('@A class X {}').to.equal(`@A 7 | class X {}`); 8 | }); 9 | it('translates plain decorators when applied to abstract classes', () => { 10 | expectTranslate('@A abstract class X {}').to.equal(`@A 11 | abstract class X {}`); 12 | }); 13 | it('translates arguments', () => { 14 | expectTranslate('@A(a, b) class X {}').to.equal(`@A(a, b) 15 | class X {}`); 16 | }); 17 | it('translates const arguments', () => { 18 | expectTranslate('@A([1]) class X {}').to.equal(`@A(const [1]) 19 | class X {}`); 20 | expectTranslate('@A({"a": 1}) class X {}').to.equal(`@A(const {"a": 1}) 21 | class X {}`); 22 | expectTranslate('@A(new B()) class X {}').to.equal(`@A(const B()) 23 | class X {}`); 24 | }); 25 | it('translates on functions', () => { 26 | expectTranslate('@A function f() {}').to.equal(`@A 27 | f() {}`); 28 | }); 29 | it('translates on properties', () => { 30 | expectTranslate('class X { @A p; }').to.equal(`class X { 31 | @A 32 | var p; 33 | }`); 34 | }); 35 | it('translates on parameters', 36 | () => { expectTranslate('function f (@A p) {}').to.equal('f(@A p) {}'); }); 37 | it('special cases @CONST', () => { 38 | expectTranslate('@CONST class X {}').to.equal(`class X { 39 | const X(); 40 | }`); 41 | expectTranslate('@CONST() class X {}').to.equal(`class X { 42 | const X(); 43 | }`); 44 | expectTranslate(`@CONST class X { 45 | x: number; 46 | y; 47 | constructor() { super(3); this.x = 1; this.y = 2; } 48 | }`) 49 | .to.equal(`class X { 50 | final num x; 51 | final y; 52 | const X() 53 | : x = 1, 54 | y = 2, 55 | super(3); 56 | }`); 57 | 58 | // @CONST constructors. 59 | expectTranslate('@CONST class X { constructor() {} }').to.equal(`class X { 60 | const X(); 61 | }`); 62 | // For backwards-compatibility for traceur inputs (not valid TS input) 63 | expectTranslate('class X { @CONST constructor() {} }').to.equal(`class X { 64 | const X(); 65 | }`); 66 | expectErroneousCode('@CONST class X { constructor() { if (1); } }') 67 | .to.throw('const constructors can only contain assignments and super calls'); 68 | expectErroneousCode('@CONST class X { constructor() { f(); } }') 69 | .to.throw('const constructors can only contain assignments and super calls'); 70 | expectErroneousCode('@CONST class X { constructor() { "string literal"; } }') 71 | .to.throw('const constructors can only contain assignments and super calls'); 72 | expectErroneousCode('class X { @CONST constructor() { x = 1; } }') 73 | .to.throw('assignments in const constructors must assign into this.'); 74 | expectErroneousCode('class X { @CONST constructor() { thax = 1; } }') 75 | .to.throw('assignments in const constructors must assign into this.'); 76 | 77 | // @CONST properties. 78 | expectTranslate('class Foo { @CONST() static foo = 1; }').to.equal(`class Foo { 79 | static const foo = 1; 80 | }`); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/e2e/angular2/src/facade/lang.d.ts: -------------------------------------------------------------------------------- 1 | export declare function CONST_EXPR(x: T): T; 2 | export declare function forwardRef(x: T): T; 3 | export declare var CONST: () => any; 4 | export declare var normalizeBlank: (x: Object) => any; 5 | -------------------------------------------------------------------------------- /test/e2e/helloworld.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import t = require('test/test'); 3 | import {MyClass, MySubclass, SOME_ARRAY} from './lib'; 4 | 5 | function callOne(a: (t: T) => U, t: T): U { 6 | return a(t); 7 | } 8 | 9 | /* tslint:disable: no-unused-variable */ 10 | function main(): void { 11 | /* tslint:enable: no-unused-variable */ 12 | t.test('handles classes', function() { 13 | let mc = new MyClass('hello'); 14 | t.expect(mc.field.toUpperCase(), t.equals('HELLO WORLD')); 15 | t.expect(mc.namedParam({x: '!'}), t.equals('hello!')); 16 | t.expect(mc.namedParam(), t.equals('hello?')); 17 | }); 18 | t.test('allows subclassing and casts', function() { 19 | let mc: MyClass; 20 | mc = new MySubclass('hello'); 21 | t.expect((mc).subclassField, t.equals('hello world')); 22 | }); 23 | t.test('string templates', function() { 24 | t.expect('$mc', t.equals('$mc')); 25 | let a = 'hello'; 26 | let b = 'world'; 27 | t.expect(`${a} ${b}`, t.equals('hello world')); 28 | }); 29 | t.test('regexp', function() { 30 | t.expect(/o\./g.test('fo.o'), t.equals(true)); 31 | t.expect(/o/g.exec('fo.o').length, t.equals(1)); 32 | t.expect(/a(b)/g.exec('ab').length, t.equals(2)); 33 | }); 34 | t.test('const expr', function() { t.expect(SOME_ARRAY[0], t.equals(1)); }); 35 | t.test('generic types fn', function() { t.expect(callOne((a) => a, 1), t.equals(1)); }); 36 | 37 | t.test('promises', function() { 38 | let p: Promise = new Promise((resolve) => { resolve(1); }); 39 | p.then((n) => { t.expect(n, t.equals(1)); }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/e2e/lib.ts: -------------------------------------------------------------------------------- 1 | import {CONST, CONST_EXPR, forwardRef} from 'angular2/src/facade/lang'; 2 | 3 | @CONST 4 | export class MyClass { 5 | private error: string = 'error'; 6 | constructor(private ctorField: string) {} 7 | 8 | get field(): string { 9 | // TODO: TypeScript doesn't parse the RHS as StringKeyword so we lose 10 | // the translation of string -> String. 11 | // We use capital S String here, even though it wouldn't run in TS. 12 | if ((' world') instanceof String) { 13 | return this.ctorField + ' world'; 14 | } else { 15 | return this.error; 16 | } 17 | } 18 | 19 | namedParam({x = '?'}: any = {}) { return 'hello' + x; } 20 | } 21 | 22 | interface Observer { 23 | update(o: Object, arg: Object); 24 | } 25 | 26 | export class MySubclass extends MyClass implements Observer { 27 | constructor(ctorField: string) { super(ctorField); } 28 | get subclassField(): string { return this.field; } 29 | update(o: Object, arg: Object) { this.field = arg.toString(); } 30 | } 31 | 32 | export const SOME_ARRAY = CONST_EXPR([1, 2, 3]); 33 | export const someArray = forwardRef(() => SOME_ARRAY); 34 | -------------------------------------------------------------------------------- /test/e2e/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | description: Transpiled end-to-end tests for ts2dart. 3 | 4 | dependencies: 5 | test: 0.12.15+3 6 | -------------------------------------------------------------------------------- /test/e2e/test.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'test/test' { 2 | export function test(msg: string, fn: () => void); 3 | export function expect(a: any, b: any); 4 | export function equals(a: any): any; 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "baseUrl": ".", 6 | "noEmit": true, 7 | "noEmitOnError": true, 8 | "rootDir": ".", 9 | "outDir": "../../build/e2e", 10 | "target": "ES6" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/expression_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectErroneousCode, expectTranslate} from './test_support'; 3 | 4 | function expectTranslates(cases: any) { 5 | for (let tsCode of Object.keys(cases)) { 6 | expectTranslate(tsCode).to.equal(cases[tsCode]); 7 | } 8 | } 9 | 10 | // TODO(jacobr): we don't really need to be specifying separate code for the 11 | // JS and Dart version for these tests as the code formatting is identical. 12 | describe('expressions', () => { 13 | it('does math', () => { 14 | expectTranslates({ 15 | '1 + 2': '1 + 2;', 16 | '1 - 2': '1 - 2;', 17 | '1 * 2': '1 * 2;', 18 | '1 / 2': '1 / 2;', 19 | '1 % 2': '1 % 2;', 20 | 'x++': 'x++;', 21 | 'x--': 'x--;', 22 | '++x': '++x;', 23 | '--x': '--x;', 24 | '-x': '-x;', 25 | }); 26 | }); 27 | it('assigns', () => { 28 | expectTranslates({ 29 | 'x += 1': 'x += 1;', 30 | 'x -= 1': 'x -= 1;', 31 | 'x *= 1': 'x *= 1;', 32 | 'x /= 1': 'x /= 1;', 33 | 'x %= 1': 'x %= 1;', 34 | 'x <<= 1': 'x = (x as int) << 1;', 35 | 'x >>= 1': 'x = (x as int) >> 1;', 36 | // 'x >>>= 1': 'x >>>= 1;', // This doesn't appear to be a valid operator in Dart. 37 | 'x &= 1': 'x = (x as int) & 1;', 38 | 'x ^= 1': 'x = (x as int) ^ 1;', 39 | 'x |= 1': 'x = (x as int) | 1;', 40 | }); 41 | }); 42 | it('compares', () => { 43 | expectTranslates({ 44 | '1 == 2': '1 == 2;', 45 | '1 != 2': '1 != 2;', 46 | '1 > 2': '1 > 2;', 47 | '1 < 2': '1 < 2;', 48 | '1 >= 2': '1 >= 2;', 49 | '1 <= 2': '1 <= 2;', 50 | }); 51 | }); 52 | it('compares identity', () => { 53 | expectTranslate('1 === 2').to.equal('identical(1, 2);'); 54 | expectTranslate('1 !== 2').to.equal('!identical(1, 2);'); 55 | }); 56 | it('bit fiddles', () => { 57 | expectTranslates({ 58 | 'x & 2': '(x as int) & 2;', 59 | '1 & 2': '1 & 2;', 60 | '1 | 2': '1 | 2;', 61 | '1 ^ 2': '1 ^ 2;', 62 | '~1': '~1;', 63 | '1 << 2': '1 << 2;', 64 | '1 >> 2': '1 >> 2;', 65 | '0x1 & 0x2': '0x1 & 0x2;', 66 | // '1 >>> 2': '1 >>> 2;', // This doesn't appear to be a valid operator in Dart. 67 | }); 68 | }); 69 | it('translates logic', () => { 70 | expectTranslates({ 71 | '1 && 2': '1 && 2;', 72 | '1 || 2': '1 || 2;', 73 | '!1': '!1;', 74 | }); 75 | }); 76 | it('translates ternary', 77 | () => { expectTranslate('var x = 1 ? 2 : 3').to.equal('var x = 1 ? 2 : 3;'); }); 78 | it('translates the comma operator', 79 | () => { expectTranslate('var x = [1, 2]').to.equal('var x = [1, 2];'); }); 80 | it('translates "in"', 81 | () => { expectErroneousCode('x in y').to.throw('in operator is unsupported'); }); 82 | it('translates "instanceof"', 83 | () => { expectTranslate('1 instanceof Foo').to.equal('1 is Foo;'); }); 84 | it('translates "this"', () => { expectTranslate('this.x').to.equal('this.x;'); }); 85 | it('translates "delete"', 86 | () => { expectErroneousCode('delete x[y];').to.throw('delete operator is unsupported'); }); 87 | it('translates "typeof"', 88 | () => { expectErroneousCode('typeof x;').to.throw('typeof operator is unsupported'); }); 89 | it('translates "void"', 90 | () => { expectErroneousCode('void x;').to.throw('void operator is unsupported'); }); 91 | it('translates parens', () => { expectTranslate('(1)').to.equal('(1);'); }); 92 | 93 | it('translates property paths', () => { 94 | expectTranslate('foo.bar;').to.equal('foo.bar;'); 95 | expectTranslate('foo[bar];').to.equal('foo[bar];'); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/facade_converter_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {FAKE_MAIN, expectTranslate, translateSource} from './test_support'; 3 | 4 | import chai = require('chai'); 5 | 6 | function getSources(str: string): {[k: string]: string} { 7 | let srcs: {[k: string]: string} = { 8 | 'angular2/src/core/di/forward_ref.d.ts': ` 9 | export declare function forwardRef(x: T): T;`, 10 | 'node_modules/rxjs/Observable.d.ts': ` 11 | export declare class Observable {}`, 12 | 'angular2/src/facade/async.ts': ` 13 | export {Observable} from 'rxjs/Observable';`, 14 | 'angular2/src/facade/collection.ts': ` 15 | export declare var Map;`, 16 | 'angular2/src/facade/lang.d.ts': ` 17 | interface List extends Array {} 18 | export declare function CONST_EXPR(x: T): T; 19 | export declare var normalizeBlank: (x: Object) => any;`, 20 | 'other/file.ts': ` 21 | export class X { 22 | map(x: number): string { return String(x); } 23 | static get(m: any, k: string): number { return m[k]; } 24 | } 25 | export class Promise {} 26 | `, 27 | }; 28 | srcs[FAKE_MAIN] = str; 29 | return srcs; 30 | } 31 | 32 | const COMPILE_OPTS = { 33 | translateBuiltins: true, 34 | failFast: true, 35 | typingsRoot: 'some/path/to/typings/', 36 | }; 37 | 38 | function expectWithTypes(str: string) { 39 | return expectTranslate(getSources(str), COMPILE_OPTS); 40 | } 41 | 42 | function expectErroneousWithType(str: string) { 43 | return chai.expect(() => translateSource(getSources(str), COMPILE_OPTS)); 44 | } 45 | 46 | describe('type based translation', () => { 47 | describe('Dart type substitution', () => { 48 | it('finds registered substitutions', () => { 49 | expectWithTypes( 50 | 'import {Observable} from "angular2/src/facade/async"; var o: Observable;') 51 | .to.equal(`import "package:angular2/src/facade/async.dart" show Stream; 52 | 53 | Stream o;`); 54 | expectWithTypes('var p: Promise = x;').to.equal(`import "dart:async"; 55 | 56 | Future p = x;`); 57 | expectWithTypes('var y: Promise;').to.equal(`import "dart:async"; 58 | 59 | Future y;`); 60 | expectWithTypes('var n: Node;').to.equal('dynamic n;'); 61 | expectWithTypes('var xhr: XMLHttpRequest;').to.equal(`import "dart:html"; 62 | 63 | HttpRequest xhr;`); 64 | expectWithTypes('var intArray: Uint8Array;').to.equal(`import "dart:typed_arrays"; 65 | 66 | Uint8List intArray;`); 67 | expectWithTypes('var buff: ArrayBuffer;').to.equal(`import "dart:typed_arrays"; 68 | 69 | ByteBuffer buff;`); 70 | }); 71 | 72 | it('allows undeclared types', () => { expectWithTypes('var t: Thing;').to.equal('Thing t;'); }); 73 | 74 | it('does not substitute matching name from different file', () => { 75 | expectWithTypes('import {Promise} from "other/file"; var y = x instanceof Promise;') 76 | .to.equal(`import "package:other/file.dart" show Promise; 77 | 78 | var y = x is Promise;`); 79 | }); 80 | 81 | it('does not substitute all identifiers', 82 | () => { expectWithTypes('let Promise = 1;').to.equal(`var Promise = 1;`); }); 83 | }); 84 | 85 | describe('collection façade', () => { 86 | it('translates array operations to dartisms', () => { 87 | expectWithTypes('function f() { var x: Array = []; x.push(1); x.pop(); }') 88 | .to.equal(`f() { 89 | List x = []; 90 | x.add(1); 91 | x.removeLast(); 92 | }`); 93 | expectWithTypes('function f() { var x: Array = []; x.map((e) => e); }') 94 | .to.equal(`f() { 95 | List x = []; 96 | x.map((e) => e).toList(); 97 | }`); 98 | expectWithTypes('function f() { var x: Array = []; x.filter((e) => true); }') 99 | .to.equal(`f() { 100 | List x = []; 101 | x.where((e) => true).toList(); 102 | }`); 103 | expectWithTypes('function f() { var x: Array = []; x.unshift(1, 2, 3); x.shift(); }') 104 | .to.equal(`f() { 105 | List x = []; 106 | (x..insertAll(0, [1, 2, 3])).length; 107 | x.removeAt(0); 108 | }`); 109 | expectWithTypes('function f() { var x: Array = []; x.unshift(1); }').to.equal(`f() { 110 | List x = []; 111 | (x..insert(0, 1)).length; 112 | }`); 113 | expectWithTypes('function f() { var x: Array = []; x.concat([1], x).length; }') 114 | .to.equal(`f() { 115 | List x = []; 116 | (new List.from(x)..addAll([1])..addAll(x)).length; 117 | }`); 118 | expectWithTypes('var x: Array = []; var y: Array = x.slice(0);') 119 | .to.equal(`List x = []; 120 | List y = ListWrapper.slice(x, 0);`); 121 | expectWithTypes('var x: Array = []; var y: Array = x.splice(0,1);') 122 | .to.equal(`List x = []; 123 | List y = ListWrapper.splice(x, 0, 1);`); 124 | expectWithTypes('var x: Array = []; var y: string = x.join("-");') 125 | .to.equal(`List x = []; 126 | String y = x.join("-");`); 127 | expectWithTypes('var x: Array = []; var y: string = x.join();') 128 | .to.equal(`List x = []; 129 | String y = x.join(",");`); 130 | expectWithTypes('var x: Array = []; var y: number = x.find((e) => e == 0);') 131 | .to.equal(`List x = []; 132 | num y = x.firstWhere((e) => e == 0, orElse: () => null);`); 133 | expectWithTypes('var x: Array = []; var y: boolean = x.some((e) => e == 0);') 134 | .to.equal(`List x = []; 135 | bool y = x.any((e) => e == 0);`); 136 | expectWithTypes('var x: Array = []; var y: number = x.reduce((a, b) => a + b, 0);') 137 | .to.equal(`List x = []; 138 | num y = x.fold(0, (a, b) => a + b);`); 139 | expectWithTypes('var x: Array = []; var y: number = x.reduce((a, b) => a + b);') 140 | .to.equal(`List x = []; 141 | num y = x.fold(null, (a, b) => a + b);`); 142 | }); 143 | 144 | it('translates console.log', () => { 145 | expectWithTypes(`console.log(1);`).to.equal('print(1);'); 146 | expectWithTypes(`console.log(1, 2);`).to.equal('print([1, 2].join(" "));'); 147 | }); 148 | 149 | it('translates string methoids', 150 | () => { expectErroneousWithType(`var x = 'asd'.substr(0, 1);`).to.throw(/use substring/); }); 151 | 152 | it('translates map operations to dartisms', () => { 153 | expectWithTypes('function f() { var x = new Map(); x.set("k", "v"); }') 154 | .to.equal(`f() { 155 | var x = new Map(); 156 | x["k"] = "v"; 157 | }`); 158 | expectWithTypes('function f() { var x = new Map(); x.get("k"); }') 159 | .to.equal(`f() { 160 | var x = new Map(); 161 | x["k"]; 162 | }`); 163 | expectWithTypes('function f() { var x = new Map(); x.has("k"); }') 164 | .to.equal(`f() { 165 | var x = new Map(); 166 | x.containsKey("k"); 167 | }`); 168 | expectWithTypes('function f() { var x = new Map(); x.delete("k"); }') 169 | .to.equal(`f() { 170 | var x = new Map(); 171 | (x.containsKey("k") && (x.remove("k") != null || true)); 172 | }`); 173 | expectWithTypes( 174 | 'function f() { var x = new Map(); x.forEach((v, k) => null); }') 175 | .to.equal(`f() { 176 | var x = new Map(); 177 | x.forEach((k, v) => null); 178 | }`); 179 | expectWithTypes( 180 | 'function f() { var x = new Map(); x.forEach(function (v, k) { return null; }); }') 181 | .to.equal(`f() { 182 | var x = new Map(); 183 | x.forEach((k, v) { 184 | return null; 185 | }); 186 | }`); 187 | expectWithTypes( 188 | 'function f() { var x = new Map(); var y = x.forEach((v, k) => { return null; }); }') 189 | .to.equal(`f() { 190 | var x = new Map(); 191 | var y = x.forEach((k, v) { 192 | return null; 193 | }); 194 | }`); 195 | 196 | expectWithTypes('function f() { var x = new Map(); x.forEach(fn); }') 197 | .to.equal(`f() { 198 | var x = new Map(); 199 | x.forEach((k, v) => (fn)(v, k)); 200 | }`); 201 | }); 202 | 203 | it('translates map properties to dartisms', () => { 204 | expectWithTypes('var x = new Map();var y = x.size;') 205 | .to.equal(`var x = new Map(); 206 | var y = x.length;`); 207 | }); 208 | }); 209 | 210 | describe('regexp', () => { 211 | expectWithTypes('function f() { var x = /a/g; x.test("a"); }').to.equal(`f() { 212 | var x = new RegExp(r'a'); 213 | x.hasMatch("a"); 214 | }`); 215 | expectWithTypes('function f() { var result = /a(.)/g.exec("ab")[1]; }').to.equal(`f() { 216 | var result = new RegExp(r'a(.)').firstMatch("ab")[1]; 217 | }`); 218 | expectWithTypes('function f() { let groups = /a(.)/g.exec("ab"); }').to.equal(`f() { 219 | var groups = ((match) => new List.generate( 220 | 1 + match.groupCount, match.group))(new RegExp(r'a(.)').firstMatch("ab")); 221 | }`); 222 | expectErroneousWithType('function f() { var x = /a(.)/g; x.exec("ab")[1]; }') 223 | .to.throw( 224 | 'exec is only supported on regexp literals, ' + 225 | 'to avoid side-effect of multiple calls on global regexps.'); 226 | }); 227 | 228 | describe('promises', () => { 229 | it('translates into Futures', () => { 230 | expectWithTypes('let x: Promise = Promise.resolve(1);').to.equal(`import "dart:async"; 231 | 232 | Future x = new Future.value(1);`); 233 | expectWithTypes('let x: Promise = Promise.reject(1);').to.equal(`import "dart:async"; 234 | 235 | Future x = new Future.error(1);`); 236 | expectWithTypes('let x: Promise = new Promise((resolve) => {resolve(1);});') 237 | .to.equal(`import "dart:async"; 238 | 239 | Future x = (() { 240 | Completer _completer$$ts2dart$0 = new Completer(); 241 | var resolve = _completer$$ts2dart$0.complete; 242 | (() { 243 | resolve(1); 244 | })(); 245 | return _completer$$ts2dart$0.future; 246 | })();`); 247 | expectWithTypes('let x: Promise = new Promise((resolve, reject) => {resolve(1);});') 248 | .to.equal(`import "dart:async"; 249 | 250 | Future x = (() { 251 | Completer _completer$$ts2dart$0 = new Completer(); 252 | var resolve = _completer$$ts2dart$0.complete; 253 | var reject = _completer$$ts2dart$0.completeError; 254 | (() { 255 | resolve(1); 256 | })(); 257 | return _completer$$ts2dart$0.future; 258 | })();`); 259 | expectWithTypes('let x: Promise = new Promise((myParam1, myParam2) => {myParam1(1);});') 260 | .to.equal(`import "dart:async"; 261 | 262 | Future x = (() { 263 | Completer _completer$$ts2dart$0 = new Completer(); 264 | var myParam1 = _completer$$ts2dart$0.complete; 265 | var myParam2 = _completer$$ts2dart$0.completeError; 266 | (() { 267 | myParam1(1); 268 | })(); 269 | return _completer$$ts2dart$0.future; 270 | })();`); 271 | expectWithTypes( 272 | 'let x: Promise = new Promise((resolve, reject) => {resolve(1);});' + 273 | 'function fn(): void { x.then((v) => { console.log(v) }).catch((err) => { console.log(err); }); }') 274 | .to.equal(`import "dart:async"; 275 | 276 | Future x = (() { 277 | Completer _completer$$ts2dart$0 = new Completer(); 278 | var resolve = _completer$$ts2dart$0.complete; 279 | var reject = _completer$$ts2dart$0.completeError; 280 | (() { 281 | resolve(1); 282 | })(); 283 | return _completer$$ts2dart$0.future; 284 | })(); 285 | void fn() { 286 | x.then((v) { 287 | print(v); 288 | }).catchError((err) { 289 | print(err); 290 | }); 291 | }`); 292 | expectWithTypes( 293 | 'var fn: () => Promise;' + 294 | 'function main() { fn().then((v) => { console.log(v) }).catch((err) => { console.log(err); }); }') 295 | .to.equal(`import "dart:async"; 296 | 297 | dynamic /* () => Promise */ fn; 298 | main() { 299 | fn().then((v) { 300 | print(v); 301 | }).catchError((err) { 302 | print(err); 303 | }); 304 | }`); 305 | expectWithTypes( 306 | 'var fn: () => Promise;' + 307 | 'function main() { fn().then((v) => { console.log(v) }, (err) => { console.log(err); }); }') 308 | .to.equal(`import "dart:async"; 309 | 310 | dynamic /* () => Promise */ fn; 311 | main() { 312 | fn().then((v) { 313 | print(v); 314 | }).catchError((err) { 315 | print(err); 316 | }); 317 | }`); 318 | }); 319 | }); 320 | 321 | describe( 322 | 'builtin functions', () => { 323 | it('translates CONST_EXPR(...) to const (...)', () => { 324 | expectWithTypes( 325 | 'import {CONST_EXPR} from "angular2/src/facade/lang";\n' + 326 | 'const x = CONST_EXPR(1);') 327 | .to.equal('const x = 1;'); 328 | expectWithTypes( 329 | 'import {CONST_EXPR} from "angular2/src/facade/lang";\n' + 330 | 'const x = CONST_EXPR([]);') 331 | .to.equal('const x = const [];'); 332 | expectWithTypes( 333 | 'import {CONST_EXPR} from "angular2/src/facade/lang";\n' + 334 | 'class Person {}' + 335 | 'const x = CONST_EXPR(new Person());') 336 | .to.equal(`class Person {} 337 | 338 | const x = const Person();`); 339 | expectWithTypes( 340 | 'import {CONST_EXPR} from "angular2/src/facade/lang";\n' + 341 | 'const x = CONST_EXPR({"one":1});') 342 | .to.equal('const x = const {"one": 1};'); 343 | expectWithTypes( 344 | 'import {CONST_EXPR} from "angular2/src/facade/lang";\n' + 345 | 'import {Map} from "angular2/src/facade/collection";\n' + 346 | 'const x = CONST_EXPR(new Map());') 347 | .to.equal(`import "package:angular2/src/facade/collection.dart" show Map; 348 | 349 | const x = const {};`); 350 | expectWithTypes( 351 | 'import {CONST_EXPR} from "angular2/src/facade/lang";\n' + 352 | 'import {Map} from "angular2/src/facade/collection";\n' + 353 | 'const x = CONST_EXPR(new Map());') 354 | .to.equal(`import "package:angular2/src/facade/collection.dart" show Map; 355 | 356 | const x = const {};`); 357 | 358 | expectWithTypes(` 359 | import {CONST_EXPR} from "angular2/src/facade/lang"; 360 | const _EMPTY_LIST = CONST_EXPR([]);`) 361 | .to.equal(`const _EMPTY_LIST = const [];`); 362 | expectWithTypes(` 363 | import {CONST_EXPR} from "angular2/src/facade/lang"; 364 | const _EMPTY_LIST = CONST_EXPR([]);`) 365 | .to.equal(`const _EMPTY_LIST = const [];`); 366 | expectWithTypes(` 367 | import {CONST_EXPR} from "angular2/src/facade/lang"; 368 | const MY_MAP = CONST_EXPR(<{[k: string]: number}>{});`) 369 | .to.equal(`const MY_MAP = const {};`); 370 | }); 371 | 372 | it('translates comment /* @ts2dart_const */ (...) to const (...)', () => { 373 | expectWithTypes('const x = /* @ts2dart_const */ (1);').to.equal('const x = (1);'); 374 | expectWithTypes('const x = /* @ts2dart_const */ 1 + 2;').to.equal('const x = 1 + 2;'); 375 | expectWithTypes(`const x = /* @ts2dart_const */ [];`).to.equal('const x = const [];'); 376 | // Nested expressions. 377 | expectWithTypes(`const x = /* @ts2dart_const */ [[1]];`).to.equal(`const x = const [ 378 | const [1] 379 | ];`); 380 | }); 381 | 382 | it('translates forwardRef(() => T) to T', 383 | () => { 384 | expectWithTypes( 385 | 'import {forwardRef} from "angular2/src/core/di/forward_ref";\n' + 386 | 'var SomeType = 1;\n' + 387 | 'var x = forwardRef(() => SomeType);') 388 | .to.equal(`var SomeType = 1; 389 | var x = SomeType;`); 390 | expectErroneousWithType(`import {forwardRef} from "angular2/src/core/di/forward_ref"; 391 | forwardRef(1)`).to.throw(/only arrow functions/); 392 | }); 393 | 394 | it('erases calls to normalizeBlank', () => { 395 | expectWithTypes( 396 | 'import {normalizeBlank} from "angular2/src/facade/lang";\n' + 397 | 'var x = normalizeBlank([]);') 398 | .to.equal('var x = [];'); 399 | }); 400 | }); 401 | 402 | it('translates array façades', () => { 403 | expectWithTypes('function f() { var x = []; Array.isArray(x); }').to.equal(`f() { 404 | var x = []; 405 | ((x) is List); 406 | }`); 407 | }); 408 | 409 | describe('error detection', () => { 410 | describe('Map', () => { 411 | it('.forEach() should report an error when the callback doesn\'t have 2 args', () => { 412 | expectErroneousWithType('var x = new Map(); x.forEach((v, k, m) => null);') 413 | .to.throw('Map.forEach callback requires exactly two arguments'); 414 | }); 415 | }); 416 | 417 | describe('Array', () => { 418 | it('.concat() should report an error if any arg is not an Array', () => { 419 | expectErroneousWithType('var x: Array = []; x.concat(1);') 420 | .to.throw('Array.concat only takes Array arguments'); 421 | }); 422 | }); 423 | 424 | it('for untyped symbols matching special cased fns', () => { 425 | expectErroneousWithType('forwardRef(1)').to.throw(/Untyped property access to "forwardRef"/); 426 | }); 427 | 428 | it('for untyped symbols matching special cased methods', () => { 429 | expectErroneousWithType('x.push(1)').to.throw(/Untyped property access to "push"/); 430 | }); 431 | 432 | it('allows unrelated methods', () => { 433 | expectWithTypes( 434 | 'import {X} from "other/file";\n' + 435 | 'var y = new X().map(1)') 436 | .to.equal(`import "package:other/file.dart" show X; 437 | 438 | var y = new X().map(1);`); 439 | expectWithTypes(`import {X} from "other/file"; 440 | var y = X.get({"a": 1}, "a");`) 441 | .to.equal(`import "package:other/file.dart" show X; 442 | 443 | var y = X.get({"a": 1}, "a");`); 444 | expectWithTypes('["a", "b"].map((x) => x);').to.equal('["a", "b"].map((x) => x).toList();'); 445 | }); 446 | }); 447 | }); 448 | 449 | describe('@ts2dart_Provider', () => { 450 | it('transforms expressions', () => { 451 | expectWithTypes(` 452 | var x = /* @ts2dart_Provider */ { 453 | provide: SomeThing, useClass: XHRImpl, multi: true 454 | };`).to.equal(`import "package:angular2/core.dart" show Provider; 455 | 456 | var x = const Provider(SomeThing, useClass: XHRImpl, multi: true);`); 457 | }); 458 | 459 | it('does not add multiple imports', () => { 460 | expectWithTypes(` 461 | import {Provider} from 'angular2/core'; 462 | var x = /* @ts2dart_Provider */ {provide: SomeThing, useClass: X};`) 463 | .to.equal(`import "package:angular2/core.dart" show Provider; 464 | 465 | var x = const Provider(SomeThing, useClass: X);`); 466 | }); 467 | }); 468 | -------------------------------------------------------------------------------- /test/function_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectErroneousCode, expectTranslate} from './test_support'; 3 | 4 | describe('functions', () => { 5 | it('supports declarations', () => { expectTranslate('function x() {}').to.equal('x() {}'); }); 6 | it('supports param default values', () => { 7 | expectTranslate('function x(a = 42, b = 1) { return 42; }').to.equal(`x([a = 42, b = 1]) { 8 | return 42; 9 | }`); 10 | expectTranslate('function x(p1, a = 42, b = 1, p2) { return 42; }') 11 | .to.equal(`x(p1, [a = 42, b = 1, p2]) { 12 | return 42; 13 | }`); 14 | expectTranslate('function x(a = [], b = {}, c = new C()) { return 42; }') 15 | .to.equal(`x([a = const [], b = const {}, c = const C()]) { 16 | return 42; 17 | }`); 18 | }); 19 | it('translates optional parameters', () => { 20 | expectTranslate('function x(a?: number, b?: number) { return 42; }') 21 | .to.equal(`x([num a, num b]) { 22 | return 42; 23 | }`); 24 | expectTranslate('function x(p1, a?: number, b?: number, p2) { return 42; }') 25 | .to.equal(`x(p1, [num a, num b, p2]) { 26 | return 42; 27 | }`); 28 | }); 29 | it('supports empty returns', () => { 30 | expectTranslate('function x() { return; }').to.equal(`x() { 31 | return; 32 | }`); 33 | }); 34 | 35 | it('does not support var args', () => { 36 | expectErroneousCode('function x(...a: number) { return 42; }') 37 | .to.throw('rest parameters are unsupported'); 38 | }); 39 | it('translates function expressions', 40 | () => { expectTranslate('var a = function() {}').to.equal('var a = () {};'); }); 41 | it('translates fat arrow operator', () => { 42 | expectTranslate('var a = () => {}').to.equal('var a = () {};'); 43 | expectTranslate('var a = (): string => {}').to.equal('var a = /* String */ () {};'); 44 | expectTranslate('var a = (p) => isBlank(p)').to.equal('var a = (p) => isBlank(p);'); 45 | expectTranslate('var a = (p = null) => isBlank(p)') 46 | .to.equal('var a = ([p = null]) => isBlank(p);'); 47 | }); 48 | it('translates types on function expressions', () => { 49 | expectTranslate('let a = function(p: string): string { return p; };') 50 | .to.equal(`var a = /* String */ (String p) { 51 | return p; 52 | };`); 53 | }); 54 | it('supports function parameters', () => { 55 | expectTranslate('function f(fn: (a: A, b: B) => C) {}').to.equal('f(C fn(A a, B b)) {}'); 56 | }); 57 | it('supports recursive function parameters', () => { 58 | expectTranslate('function f(fn: (a: (b: B) => C) => D) {}').to.equal('f(D fn(C a(B b))) {}'); 59 | }); 60 | it('supports generic-typed function parameters', () => { 61 | expectTranslate('function f(fn: (a: T, b: U) => T) {}', { 62 | translateBuiltins: true 63 | }).to.equal('f/*< T, U >*/(dynamic/*= T */ fn(dynamic/*= T */ a, dynamic/*= U */ b)) {}'); 64 | }); 65 | it('translates functions taking rest parameters to untyped Function', () => { 66 | expectTranslate('function f(fn: (...a: string[]) => number) {}').to.equal('f(Function fn) {}'); 67 | }); 68 | }); 69 | 70 | describe('named parameters', () => { 71 | it('supports named parameters', () => { 72 | expectTranslate('function x({a = "x", b}) { return a + b; }', { 73 | translateBuiltins: true 74 | }).to.equal(`x({a: "x", b}) { 75 | return a + b; 76 | }`); 77 | }); 78 | it('supports types on named parameters', () => { 79 | expectTranslate('function x({a = 1, b = 2}: {a: number, b: number} = {}) { return a + b; }', { 80 | translateBuiltins: true 81 | }).to.equal(`x({num a: 1, num b: 2}) { 82 | return a + b; 83 | }`); 84 | }); 85 | it('supports reference types on named parameters', () => { 86 | expectTranslate( 87 | 'interface Args { a: string; b: number }\n' + 88 | 'function x({a, b, c}: Args) { return a + b; }', 89 | {translateBuiltins: true}) 90 | .to.equal(`abstract class Args { 91 | String a; 92 | num b; 93 | } 94 | 95 | x({String a, num b, c}) { 96 | return a + b; 97 | }`); 98 | }); 99 | it('supports declared, untyped named parameters', () => { 100 | expectTranslate('function x({a, b}: {a: number, b}) { return a + b; }', { 101 | translateBuiltins: true 102 | }).to.equal(`x({num a, b}) { 103 | return a + b; 104 | }`); 105 | }); 106 | it('fails for non-property types on named parameters', () => { 107 | expectErroneousCode( 108 | 'interface X { a(a: number); }\n' + 109 | 'function x({a}: X) { return a + b; }', 110 | {translateBuiltins: true}) 111 | .to.throw('X.a used for named parameter definition must be a property'); 112 | }); 113 | }); 114 | 115 | describe('generic functions', () => { 116 | it('supports generic types', () => { 117 | expectTranslate('function sort(xs: T[]): T[] { return xs; }', { 118 | translateBuiltins: true 119 | }).to.equal(`List sort/*< T, U >*/(List xs) { 120 | return xs; 121 | }`); 122 | expectTranslate('function inGeneric(x: T, y: Y): T { return x; }', { 123 | translateBuiltins: true 124 | }).to.equal(`dynamic/*= T */ inGeneric/*< T, U >*/( 125 | dynamic/*= T */ x, Y y) { 126 | return x; 127 | }`); 128 | expectTranslate('class X { sort(xs: T[]): T[] { return xs; } }', { 129 | translateBuiltins: true 130 | }).to.equal(`class X { 131 | List sort/*< T, U >*/(List xs) { 132 | return xs; 133 | } 134 | }`); 135 | }); 136 | it('replaces type usage sites, but not idents', () => { 137 | expectTranslate( 138 | `function wobble(u: U): T { 139 | let t: T = u; 140 | for (let T of [1, 2]) {} 141 | return t; 142 | }`, 143 | {translateBuiltins: true}) 144 | .to.equal(`dynamic/*= T */ wobble/*< T, U >*/(dynamic/*= U */ u) { 145 | dynamic/*= T */ t = (u as dynamic/*= T */); 146 | for (var T in [1, 2]) {} 147 | return t; 148 | }`); 149 | }); 150 | it('translates generic calls', () => { 151 | expectTranslate( 152 | `function wobble(foo: T): T { return foo; } 153 | let f = foo('hello');`, 154 | {translateBuiltins: true}) 155 | .to.equal(`dynamic/*= T */ wobble/*< T >*/(dynamic/*= T */ foo) { 156 | return foo; 157 | } 158 | 159 | var f = foo/*< String >*/("hello");`); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/literal_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectErroneousCode, expectTranslate} from './test_support'; 3 | 4 | describe('literals', () => { 5 | it('translates string literals', () => { 6 | expectTranslate(`'hello\\' "world'`).to.equal(`"hello' \\"world";`); 7 | expectTranslate(`"hello\\" 'world"`).to.equal(`"hello\\" 'world";`); 8 | }); 9 | 10 | it('translates string templates', () => { 11 | expectTranslate('`hello \nworld`').to.equal(`'''hello \nworld''';`); 12 | expectTranslate('`hello ${world}`').to.equal(`'''hello \${ world}''';`); 13 | expectTranslate('`${a}$b${$c}`').to.equal(`'''\${ a}\\$b\${ $c}''';`); 14 | expectTranslate('`\'${a}\'`').to.equal(`'''\\\'\${ a}\\\'''';`); 15 | expectTranslate('`\'a\'`').to.equal(`'''\\\'a\\\'''';`); 16 | // https://github.com/angular/angular/issues/509 17 | expectTranslate('"${a}"').to.equal('"\\${a}";'); 18 | expectTranslate('"\\${a}"').to.equal('"\\${a}";'); 19 | expectTranslate('\'\\${a}\'').to.equal('"\\${a}";'); 20 | expectTranslate('\'$a\'').to.equal(`"\\$a";`); 21 | expectTranslate('`$a`').to.equal(`'''\\$a''';`); 22 | expectTranslate('`\\$a`').to.equal(`'''\\$a''';`); 23 | }); 24 | 25 | it('escapes escape sequences', 26 | () => { expectTranslate('`\\\\u1234`').to.equal(`'''\\\\u1234''';`); }); 27 | 28 | it('translates boolean literals', () => { 29 | expectTranslate('true').to.equal('true;'); 30 | expectTranslate('false').to.equal('false;'); 31 | expectTranslate('var b:boolean = true;').to.equal('bool b = true;'); 32 | }); 33 | 34 | it('translates the null literal', () => { expectTranslate('null').to.equal('null;'); }); 35 | 36 | it('translates number literals', () => { 37 | // Negative numbers are handled by unary minus expressions. 38 | expectTranslate('1234').to.equal('1234;'); 39 | expectTranslate('12.34').to.equal('12.34;'); 40 | expectTranslate('1.23e-4').to.equal('1.23e-4;'); 41 | }); 42 | 43 | it('translates regexp literals', () => { 44 | expectTranslate('/wo\\/t?/g').to.equal('new RegExp(r\'wo\\/t?\');'); 45 | expectTranslate('/\'/g').to.equal('new RegExp(r\'\' + "\'" + r\'\');'); 46 | expectTranslate('/\'o\'/g').to.equal('new RegExp(r\'\' + "\'" + r\'o\' + "\'" + r\'\');'); 47 | expectTranslate('/abc/gmi') 48 | .to.equal('new RegExp(r\'abc\', multiLine: true, caseSensitive: false);'); 49 | expectErroneousCode('/abc/').to.throw(/Regular Expressions must use the \/\/g flag/); 50 | }); 51 | 52 | it('translates array literals', () => { 53 | expectTranslate('[1,2]').to.equal('[1, 2];'); 54 | expectTranslate('[1,]').to.equal('[1];'); 55 | expectTranslate('[]').to.equal('[];'); 56 | }); 57 | 58 | it('translates object literals', () => { 59 | expectTranslate('var x = {a: 1, b: 2}').to.equal('var x = {"a": 1, "b": 2};'); 60 | expectTranslate('var x = {a: 1, }').to.equal('var x = {"a": 1};'); 61 | expectTranslate('var x = {}').to.equal('var x = {};'); 62 | expectTranslate('var x = {y}').to.equal('var x = {"y": y};'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/main_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | import chai = require('chai'); 5 | import main = require('../lib/main'); 6 | 7 | import {expectTranslate, expectErroneousCode} from './test_support'; 8 | 9 | describe('main transpiler functionality', () => { 10 | describe('comments', () => { 11 | it('keeps leading comments', () => { 12 | expectTranslate(` 13 | function f() { 14 | /* A */ a; 15 | /* B */ b; 16 | }`).to.equal(`f() { 17 | /* A */ a; 18 | /* B */ b; 19 | }`); 20 | expectTranslate(`function f() { 21 | // A 22 | a 23 | // B 24 | b 25 | }`).to.equal(`f() { 26 | // A 27 | a; 28 | // B 29 | b; 30 | }`); 31 | }); 32 | it('keeps ctor comments', () => { 33 | expectTranslate('/** A */ class A {\n /** ctor */ constructor() {}}').to.equal(`/** A */ 34 | class A { 35 | /** ctor */ A() {} 36 | }`); 37 | }); 38 | it('translates links to dart doc format', () => { 39 | expectTranslate('/** {@link this/place} */ a').to.equal('/** [this/place] */ a;'); 40 | expectTranslate('/* {@link 1} {@link 2} */ a').to.equal('/* [1] [2] */ a;'); 41 | }); 42 | it('removes @module doc tags', () => { 43 | expectTranslate(`/** @module 44 | * This is a module for doing X. 45 | */`).to.equal(`/** 46 | * This is a module for doing X. 47 | */`); 48 | }); 49 | it('removes @description doc tags', () => { 50 | expectTranslate(`/** @description 51 | * This is a module for doing X. 52 | */`).to.equal(`/** 53 | * This is a module for doing X. 54 | */`); 55 | }); 56 | it('removes @depracted doc tags', () => { 57 | expectTranslate(`/** 58 | * Use SomethingElse instead. 59 | * @deprecated 60 | */`).to.equal(`/** 61 | * Use SomethingElse instead. 62 | * 63 | */`); 64 | }); 65 | it('removes @param doc tags', () => { 66 | expectTranslate(`/** 67 | * Method to do blah. 68 | * @param doc Document. 69 | */`).to.equal(`/** 70 | * Method to do blah. 71 | * 72 | */`); 73 | }); 74 | it('removes @return doc tags', () => { 75 | expectTranslate(`/** 76 | * Method to do blah. 77 | * @return {String} 78 | */`).to.equal(`/** 79 | * Method to do blah. 80 | * 81 | */`); 82 | }); 83 | it('removes @throws doc tags', () => { 84 | expectTranslate(`/** 85 | * Method to do blah. 86 | * @throws ArgumentException If arguments are wrong 87 | */`).to.equal(`/** 88 | * Method to do blah. 89 | * 90 | */`); 91 | }); 92 | }); 93 | 94 | describe('errors', () => { 95 | it('reports multiple errors', () => { 96 | // Reports both the private field not having an underbar and protected being unsupported. 97 | let errorLines = new RegExp( 98 | 'delete operator is unsupported\n' + 99 | '.*void operator is unsupported'); 100 | expectErroneousCode('delete x["y"]; void z;').to.throw(errorLines); 101 | }); 102 | it('reports relative paths in errors', () => { 103 | chai.expect(() => expectTranslate({'/a/b/c.ts': 'delete x["y"];'}, {basePath: '/a'})) 104 | .to.throw(/^b\/c.ts:1/); 105 | }); 106 | it('reports errors across multiple files', () => { 107 | expectErroneousCode({'a.ts': 'delete x["y"];', 'b.ts': 'delete x["y"];'}, { 108 | failFast: false 109 | }).to.throw(/^a\.ts.*\nb\.ts/); 110 | }); 111 | }); 112 | 113 | describe('output paths', () => { 114 | it('writes within the path', () => { 115 | let transpiler = new main.Transpiler({basePath: '/a'}); 116 | chai.expect(transpiler.getOutputPath('/a/b/c.js', '/x')).to.equal('/x/b/c.dart'); 117 | chai.expect(transpiler.getOutputPath('b/c.js', '/x')).to.equal('/x/b/c.dart'); 118 | chai.expect(transpiler.getOutputPath('b/c.js', 'x')).to.equal('x/b/c.dart'); 119 | chai.expect(() => transpiler.getOutputPath('/outside/b/c.js', '/x')) 120 | .to.throw(/must be located under base/); 121 | }); 122 | it('defaults to writing to the full path', () => { 123 | let transpiler = new main.Transpiler({basePath: undefined}); 124 | chai.expect(transpiler.getOutputPath('/a/b/c.js', '/e')).to.equal('/e/a/b/c.dart'); 125 | chai.expect(transpiler.getOutputPath('b/c.js', '')).to.equal('b/c.dart'); 126 | }); 127 | it('translates .es6, .ts, and .js', () => { 128 | let transpiler = new main.Transpiler({basePath: undefined}); 129 | ['a.js', 'a.ts', 'a.es6'].forEach( 130 | (n) => { chai.expect(transpiler.getOutputPath(n, '')).to.equal('a.dart'); }); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/module_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import chai = require('chai'); 3 | import main = require('../lib/main'); 4 | import ModuleTranspiler from '../lib/module'; 5 | import {FacadeConverter} from '../lib/facade_converter'; 6 | 7 | import {expectTranslate, expectErroneousCode, translateSources} from './test_support'; 8 | 9 | describe('imports', () => { 10 | it('translates import equals statements', () => { 11 | expectTranslate('import x = require("y");').to.equal('import "package:y.dart" as x;'); 12 | }); 13 | it('translates import from statements', () => { 14 | expectTranslate('import {x,y} from "z";').to.equal('import "package:z.dart" show x, y;'); 15 | }); 16 | it('translates import star', () => { 17 | expectTranslate('import * as foo from "z";').to.equal('import "package:z.dart" as foo;'); 18 | }); 19 | it('allows import dart file from relative path', () => { 20 | expectTranslate('import x = require("./y")').to.equal('import "y.dart" as x;'); 21 | expectTranslate('import {x} from "./y"').to.equal('import "y.dart" show x;'); 22 | expectTranslate('import {x} from "../y"').to.equal('import "../y.dart" show x;'); 23 | }); 24 | it('handles ignored annotations in imports', () => { 25 | expectTranslate('import {CONST, CONST_EXPR, IMPLEMENTS, ABSTRACT} from "x"').to.equal(''); 26 | expectTranslate('import {x, IMPLEMENTS} from "./x"').to.equal('import "x.dart" show x;'); 27 | }); 28 | it('fails for renamed imports', () => { 29 | expectErroneousCode('import {Foo as Bar} from "baz";') 30 | .to.throw(/import\/export renames are unsupported in Dart/); 31 | }); 32 | it('fails for empty import specs', 33 | () => { expectErroneousCode('import {} from "baz";').to.throw(/empty import list/); }); 34 | it('translates angular/ references to angular2/', () => { 35 | expectTranslate(`import {foo} from '@angular/foo';`) 36 | .to.equal(`import "package:angular2/foo.dart" show foo;`); 37 | }); 38 | }); 39 | 40 | describe('exports', () => { 41 | // Dart exports are implicit, everything non-private is exported by the library. 42 | it('allows variable exports', 43 | () => { expectTranslate('export var x = 12;').to.equal('var x = 12;'); }); 44 | it('allows class exports', 45 | () => { expectTranslate('export class X {}').to.equal('class X {}'); }); 46 | it('allows export declarations', 47 | () => { expectTranslate('export * from "X";').to.equal('export "package:X.dart";'); }); 48 | it('allows export declarations', 49 | () => { expectTranslate('export * from "./X";').to.equal('export "X.dart";'); }); 50 | it('allows named export declarations', () => { 51 | expectTranslate('export {a, b} from "X";').to.equal('export "package:X.dart" show a, b;'); 52 | }); 53 | it('fails for renamed exports', () => { 54 | expectErroneousCode('export {Foo as Bar} from "baz";') 55 | .to.throw(/import\/export renames are unsupported in Dart/); 56 | }); 57 | it('fails for exports without URLs', () => { 58 | expectErroneousCode('export {a as b};').to.throw('re-exports must have a module URL'); 59 | }); 60 | it('fails for empty export specs', 61 | () => { expectErroneousCode('export {} from "baz";').to.throw(/empty export list/); }); 62 | }); 63 | 64 | describe('library name', () => { 65 | let transpiler: main.Transpiler; 66 | let modTranspiler: ModuleTranspiler; 67 | beforeEach(() => { 68 | transpiler = new main.Transpiler({failFast: true, generateLibraryName: true, basePath: '/a'}); 69 | modTranspiler = new ModuleTranspiler(transpiler, new FacadeConverter(transpiler), true); 70 | }); 71 | it('adds a library name', () => { 72 | let results = translateSources( 73 | {'/a/b/c.ts': 'var x;'}, {failFast: true, generateLibraryName: true, basePath: '/a'}); 74 | chai.expect(results['/a/b/c.ts']).to.equal(`library b.c; 75 | 76 | var x; 77 | `); 78 | }); 79 | it('leaves relative paths alone', 80 | () => { chai.expect(modTranspiler.getLibraryName('a/b')).to.equal('a.b'); }); 81 | it('strips leading @ signs', 82 | () => { chai.expect(modTranspiler.getLibraryName('@a/b')).to.equal('a.b'); }); 83 | it('handles reserved words', () => { 84 | chai.expect(modTranspiler.getLibraryName('/a/for/in/do/x')).to.equal('_for._in._do.x'); 85 | }); 86 | it('handles built-in and limited keywords', () => { 87 | chai.expect(modTranspiler.getLibraryName('/a/as/if/sync/x')).to.equal('as._if.sync.x'); 88 | }); 89 | it('handles file extensions', () => { 90 | chai.expect(modTranspiler.getLibraryName('a/x.ts')).to.equal('a.x'); 91 | chai.expect(modTranspiler.getLibraryName('a/x.js')).to.equal('a.x'); 92 | }); 93 | it('handles non word characters', 94 | () => { chai.expect(modTranspiler.getLibraryName('a/%x.ts')).to.equal('a._x'); }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/statement_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectTranslate} from './test_support'; 3 | 4 | describe('statements', () => { 5 | it('translates switch', () => { 6 | expectTranslate('switch(x) { case 1: break; case 2: break; default: break; }') 7 | .to.equal(`switch (x) { 8 | case 1: 9 | break; 10 | case 2: 11 | break; 12 | default: 13 | break; 14 | }`); 15 | }); 16 | it('translates for loops', () => { 17 | expectTranslate('for (1; 2; 3) { 4 }').to.equal(`for (1; 2; 3) { 18 | 4; 19 | }`); 20 | expectTranslate('for (var x = 1; 2; 3) { 4 }').to.equal(`for (var x = 1; 2; 3) { 21 | 4; 22 | }`); 23 | expectTranslate('for (var x, y = 1; 2; 3) { 4 }').to.equal(`for (var x, y = 1; 2; 3) { 24 | 4; 25 | }`); 26 | expectTranslate('for (var x = 0, y = 1; 2; 3) { 4 }').to.equal(`for (var x = 0, y = 1; 2; 3) { 27 | 4; 28 | }`); 29 | }); 30 | it('translates for-in loops', () => { 31 | expectTranslate('for (var x in 1) { 2 }').to.equal(`for (var x in 1) { 32 | 2; 33 | }`); 34 | expectTranslate('for (x in 1) { 2 }').to.equal(`for (x in 1) { 35 | 2; 36 | }`); 37 | }); 38 | it('translates for-of loops', () => { 39 | expectTranslate('for (var x of 1) { 2 }').to.equal(`for (var x in 1) { 40 | 2; 41 | }`); 42 | expectTranslate('for (x of 1) { 2 }').to.equal(`for (x in 1) { 43 | 2; 44 | }`); 45 | }); 46 | it('translates while loops', () => { 47 | expectTranslate('while (1) { 2 }').to.equal(`while (1) { 48 | 2; 49 | }`); 50 | expectTranslate('do 1; while (2);').to.equal('do 1; while (2);'); 51 | }); 52 | it('translates if/then/else', () => { 53 | expectTranslate('if (x) { 1 }').to.equal(`if (x) { 54 | 1; 55 | }`); 56 | expectTranslate('if (x) { 1 } else { 2 }').to.equal(`if (x) { 57 | 1; 58 | } else { 59 | 2; 60 | }`); 61 | expectTranslate('if (x) 1;').to.equal('if (x) 1;'); 62 | expectTranslate('if (x) 1; else 2;').to.equal(`if (x) 63 | 1; 64 | else 65 | 2;`); 66 | }); 67 | it('translates try/catch', () => { 68 | expectTranslate('try {} catch(e) {} finally {}') 69 | .to.equal('try {} catch (e, e_stack) {} finally {}'); 70 | expectTranslate('try {} catch(e: MyException) {}') 71 | .to.equal('try {} on MyException catch (e, e_stack) {}'); 72 | }); 73 | it('translates throw', 74 | () => { expectTranslate('throw new Error("oops")').to.equal('throw new Error("oops");'); }); 75 | it('translates empty statements', () => { expectTranslate(';').to.equal(';'); }); 76 | it('translates break & continue', () => { 77 | expectTranslate(`while (true) { 78 | break; 79 | }`).to.equal(`while (true) { 80 | break; 81 | }`); 82 | expectTranslate(`while (true) { 83 | continue; 84 | }`).to.equal(`while (true) { 85 | continue; 86 | }`); 87 | expectTranslate(`while (true) { 88 | break foo; 89 | }`).to.equal(`while (true) { 90 | break foo; 91 | }`); 92 | }); 93 | it('rewrites catch block to preserve stack trace', () => { 94 | expectTranslate(`try {} catch (e) { 95 | console.log(e, e.stack); 96 | }`).to.equal(`try {} catch (e, e_stack) { 97 | console.log(e, e_stack); 98 | }`); 99 | }); 100 | it('rewrites rethrow to preserve stack trace', () => { 101 | expectTranslate('try {} catch (ex) { throw ex; }').to.equal(`try {} catch (ex, ex_stack) { 102 | rethrow; 103 | }`); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/test_support.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | import chai = require('chai'); 5 | import fs = require('fs'); 6 | import main = require('../lib/main'); 7 | import ts = require('typescript'); 8 | 9 | export type StringMap = { 10 | [k: string]: string 11 | }; 12 | export type Input = string | StringMap; 13 | 14 | export function expectTranslate(tsCode: Input, options: main.TranspilerOptions = {}) { 15 | let result = translateSource(tsCode, options); 16 | // The Dart formatter is aggressive at terminating statements with \n 17 | // which clutters the expectation output without providing value. 18 | if (result[result.length - 1] === '\n') { 19 | result = result.slice(0, -1); 20 | } 21 | return chai.expect(result); 22 | } 23 | 24 | export function expectErroneousCode(tsCode: Input, options: main.TranspilerOptions = {}) { 25 | options.failFast = false; // Collect *all* errors. 26 | return chai.expect(() => translateSource(tsCode, options)); 27 | } 28 | 29 | let compilerOptions = main.COMPILER_OPTIONS; 30 | let defaultLibPath = ts.getDefaultLibFilePath(compilerOptions); 31 | let libSource = fs.readFileSync(ts.getDefaultLibFilePath(compilerOptions), 'utf-8'); 32 | let libSourceFile: ts.SourceFile; 33 | 34 | export function parseFiles(nameToContent: StringMap): [ts.Program, ts.CompilerHost] { 35 | let result: string; 36 | let compilerHost: ts.CompilerHost = { 37 | getSourceFile: function(sourceName, languageVersion) { 38 | if (nameToContent.hasOwnProperty(sourceName)) { 39 | return ts.createSourceFile( 40 | sourceName, nameToContent[sourceName], compilerOptions.target, true); 41 | } 42 | if (sourceName === defaultLibPath) { 43 | if (!libSourceFile) { 44 | // Cache to avoid excessive test times. 45 | libSourceFile = ts.createSourceFile(sourceName, libSource, compilerOptions.target, true); 46 | } 47 | return libSourceFile; 48 | } 49 | return undefined; 50 | }, 51 | writeFile: function(name, text, writeByteOrderMark) { result = text; }, 52 | fileExists: (sourceName) => { return !!nameToContent[sourceName]; }, 53 | readFile: (filename): string => { throw new Error('unexpected call to readFile'); }, 54 | getDefaultLibLocation: () => defaultLibPath, 55 | getDefaultLibFileName: () => defaultLibPath, 56 | useCaseSensitiveFileNames: () => false, 57 | getCanonicalFileName: (filename) => '../' + filename, 58 | getCurrentDirectory: () => '', 59 | getNewLine: () => '\n', 60 | }; 61 | compilerHost.resolveModuleNames = main.getModuleResolver(compilerHost); 62 | // Create a program from inputs 63 | let entryPoints = Object.keys(nameToContent); 64 | let program: ts.Program = ts.createProgram(entryPoints, compilerOptions, compilerHost); 65 | if (program.getSyntacticDiagnostics().length > 0) { 66 | // Throw first error. 67 | let first = program.getSyntacticDiagnostics()[0]; 68 | let src = nameToContent[entryPoints[entryPoints.length - 1]]; 69 | throw new Error(`${first.start}: ${first.messageText} in ${src}`); 70 | } 71 | return [program, compilerHost]; 72 | } 73 | 74 | export const FAKE_MAIN = 'angular2/some/main.ts'; 75 | 76 | export function translateSources(contents: Input, options: main.TranspilerOptions = {}): StringMap { 77 | // Default to quick stack traces. 78 | if (!options.hasOwnProperty('failFast')) options.failFast = true; 79 | let namesToContent: StringMap; 80 | if (typeof contents === 'string') { 81 | namesToContent = {}; 82 | namesToContent[FAKE_MAIN] = contents; 83 | } else { 84 | namesToContent = contents; 85 | } 86 | options.enforceUnderscoreConventions = true; 87 | let transpiler = new main.Transpiler(options); 88 | let [program, host] = parseFiles(namesToContent); 89 | return transpiler.translateProgram(program, host); 90 | } 91 | 92 | 93 | export function translateSource(contents: Input, options: main.TranspilerOptions = {}): string { 94 | let results = translateSources(contents, options); 95 | // Return the main outcome, from 'main.ts'. 96 | return results[FAKE_MAIN]; 97 | } 98 | -------------------------------------------------------------------------------- /test/tsc_e2e/map_target/dep.ts: -------------------------------------------------------------------------------- 1 | export let msg = 'Hello, world!'; 2 | -------------------------------------------------------------------------------- /test/tsc_e2e/p1/user.ts: -------------------------------------------------------------------------------- 1 | import {msg} from 'mapped/dep'; 2 | 3 | function handle(v: any) { 4 | return v; 5 | } 6 | 7 | export function main() { 8 | console.log(msg); 9 | Promise.resolve(null) 10 | .then((x) => console.log(1)) 11 | .then(handle, handle) 12 | .catch((e) => console.error(e)); 13 | } 14 | -------------------------------------------------------------------------------- /test/tsc_e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "baseUrl": ".", 6 | "noEmit": true, 7 | "noEmitOnError": true, 8 | "paths": {"mapped/*": ["map_target/*"]}, 9 | "rootDir": ".", 10 | "outDir": "../../build/tsc_e2e", 11 | "target": "ES6" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/type_test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectTranslate} from './test_support'; 3 | 4 | describe('types', () => { 5 | it('supports qualified names', 6 | () => { expectTranslate('var x: foo.Bar;').to.equal('foo.Bar x;'); }); 7 | it('drops type literals', 8 | () => { expectTranslate('var x: {x: string, y: number};').to.equal('dynamic x;'); }); 9 | it('translates string index signatures to dartisms', () => { 10 | expectTranslate('var x: {[k: string]: any[]};').to.equal('Map> x;'); 11 | expectTranslate('var x: {[k: number]: number};').to.equal('Map x;'); 12 | }); 13 | it('drops type literals with index signatures and other properties', 14 | () => { expectTranslate('var x: {a: number, [k: string]: number};').to.equal('dynamic x;'); }); 15 | it('allows typecasts', () => { expectTranslate('ref').to.equal('(ref as MyType);'); }); 16 | it('translates typecasts to reified types on literals', () => { 17 | expectTranslate('let x = [];').to.equal('var x = [];'); 18 | expectTranslate('let x = <{[k:string]: number}>{};').to.equal('var x = {};'); 19 | }); 20 | it('does not mangle prototype names', () => { 21 | expectTranslate('import toString = require("./somewhere");') 22 | .to.equal('import "somewhere.dart" as toString;'); 23 | }); 24 | it('should support union types', () => { 25 | expectTranslate('var x: number|List = 11;') 26 | .to.equal('dynamic /* num | List< String > */ x = 11;'); 27 | expectTranslate('function x(): number|List<{[k: string]: any}> { return 11; }') 28 | .to.equal( 29 | 'dynamic /* num | List< Map < String , dynamic > > */ x() {\n' + 30 | ' return 11;\n' + 31 | '}'); 32 | }); 33 | it('should support array types', 34 | () => { expectTranslate('var x: string[] = [];').to.equal('List x = [];'); }); 35 | it('should support function types (by ignoring them)', () => { 36 | expectTranslate('var x: (a: string) => string;') 37 | .to.equal('dynamic /* (a: string) => string */ x;'); 38 | }); 39 | }); 40 | 41 | describe('type arguments', () => { 42 | it('should support declaration', () => { 43 | expectTranslate('class X { a: A; }').to.equal(`class X { 44 | A a; 45 | }`); 46 | }); 47 | it('should support nested extends', () => { 48 | expectTranslate('class X> { }').to.equal('class X> {}'); 49 | }); 50 | it('should multiple extends', () => { 51 | expectTranslate('class X { }') 52 | .to.equal('class X {}'); 53 | }); 54 | it('should support use', () => { 55 | expectTranslate('class X extends Y { }').to.equal('class X extends Y {}'); 56 | }); 57 | it('should remove single generic argument', () => { 58 | expectTranslate('var x: X;').to.equal('X x;'); 59 | expectTranslate('class X extends Y { }').to.equal('class X extends Y {}'); 60 | expectTranslate('var x = new Promise();').to.equal('var x = new Promise();'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "noEmit": true, 6 | "noEmitOnError": true, 7 | "noImplicitAny": true, 8 | "allowUnreachableCode": false 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "build", 13 | "test/e2e", 14 | "test/tsc_e2e" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "mocha/mocha.d.ts": { 9 | "commit": "69bdfb0884020e41f17a1dd80ad4c77de2636874" 10 | }, 11 | "chai/chai.d.ts": { 12 | "commit": "69bdfb0884020e41f17a1dd80ad4c77de2636874" 13 | }, 14 | "node/node.d.ts": { 15 | "commit": "69bdfb0884020e41f17a1dd80ad4c77de2636874" 16 | }, 17 | "source-map-support/source-map-support.d.ts": { 18 | "commit": "69bdfb0884020e41f17a1dd80ad4c77de2636874" 19 | }, 20 | "source-map/source-map.d.ts": { 21 | "commit": "2520bce9a8a71b66e67487cbd5b33fec880b0c55" 22 | }, 23 | "fs-extra/fs-extra.d.ts": { 24 | "commit": "d5f92f93bdb49f332fa662ff1d0cc8700f02e4dc" 25 | }, 26 | "minimist/minimist.d.ts": { 27 | "commit": "8b7cc13f6dbabd0a49de7ccba75c342160e600ad" 28 | }, 29 | "es6-promise/es6-promise.d.ts": { 30 | "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "forin": true, 5 | "jsdoc-format": true, 6 | "label-position": true, 7 | "label-undefined": true, 8 | "no-arg": true, 9 | "no-conditional-assignment": true, 10 | "no-construct": true, 11 | "no-debugger": true, 12 | "no-duplicate-key": true, 13 | "no-empty": true, 14 | "no-inferrable-types": true, 15 | "no-internal-module": true, 16 | "no-shadowed-variable": true, 17 | "no-string-literal": true, 18 | "no-switch-case-fall-through": true, 19 | "no-unused-expression": true, 20 | "no-unused-variable": true, 21 | "no-use-before-declare": true, 22 | "no-var-keyword": true, 23 | "radix": true, 24 | "semicolon": [true, "always"], 25 | "switch-default": true, 26 | "triple-equals": [true, "allow-null-check"], 27 | "variable-name": [true, "check-format", "ban-keywords"] 28 | } 29 | } 30 | --------------------------------------------------------------------------------