Skip to content

Commit 4aabfff

Browse files
committed
feat: façade converter handles various special cases for Dart.
* Bundles CONST_EXPR and FORWARD_REF into one place * Special cases a bunch of Array methods, allowing more idiomatic TypeScript & Dart code * Code structure should allow straight forward future additions. * Check for method names called on unknown types that might match our special cased methods, error when it happens. * Rename TranspilerStep to TranspilerBase as it is used in other places now. * Change integration test to properly check for CONST_EXPR and FORWARD_REF. Fixes
1 parent 2f0ef3e commit 4aabfff

18 files changed

+392
-114
lines changed

gulpfile.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ gulp.task('test.e2e', ['test.compile'], function(done) {
8989
fsx.copySync(__dirname + '/test/e2e', dir);
9090

9191
// run node with a shell so we can wildcard all the .ts files
92-
spawn('sh', ['-c', 'node build/lib/main.js ' + dir + '/*.ts'], {stdio: 'inherit'})
92+
var cmd = 'node ../lib/main.js --translateBuiltins --basePath=. --destination=. ' +
93+
'*.ts angular2/src/facade/lang.d.ts';
94+
// Paths must be relative to our source root, so run with cwd == dir.
95+
spawn('sh', ['-c', cmd], {stdio: 'inherit', cwd: dir})
9396
.on('close', function(code, signal) {
9497
if (code > 0) {
9598
onError(new Error("Failed to transpile " + testfile + '.ts'));

lib/base.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function ident(n: ts.Node): string {
1414
return null;
1515
}
1616

17-
export class TranspilerStep {
17+
export class TranspilerBase {
1818
constructor(private transpiler: ts2dart.Transpiler) {}
1919

2020
visit(n: ts.Node) { this.transpiler.visit(n); }
@@ -73,12 +73,13 @@ export class TranspilerStep {
7373
// TODO(martinprobst): This belongs to module.ts, refactor.
7474
getLibraryName(): string { return this.transpiler.getLibraryName(); }
7575

76-
private static DART_TYPES: {[k: string]: string} = {
76+
private static TS_TO_DART_TYPENAMES: {[k: string]: string} = {
7777
'Promise': 'Future',
7878
'Observable': 'Stream',
7979
'ObservableController': 'StreamController',
8080
'Date': 'DateTime',
81-
'StringMap': 'Map'
81+
'StringMap': 'Map',
82+
'Array': 'List',
8283
};
8384

8485
visitTypeName(typeName: ts.EntityName) {
@@ -87,8 +88,8 @@ export class TranspilerStep {
8788
return;
8889
}
8990
var identifier = ident(typeName);
90-
if (TranspilerStep.DART_TYPES.hasOwnProperty(identifier)) {
91-
identifier = TranspilerStep.DART_TYPES[identifier];
91+
if (TranspilerBase.TS_TO_DART_TYPENAMES.hasOwnProperty(identifier)) {
92+
identifier = TranspilerBase.TS_TO_DART_TYPENAMES[identifier];
9293
}
9394
this.emit(identifier);
9495
}

lib/call.ts

+6-39
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import ts = require('typescript');
33
import base = require('./base');
44
import ts2dart = require('./main');
5+
import {FacadeConverter} from "./facade_converter";
56

6-
class CallTranspiler extends base.TranspilerStep {
7-
constructor(tr: ts2dart.Transpiler) { super(tr); }
7+
class CallTranspiler extends base.TranspilerBase {
8+
constructor(tr: ts2dart.Transpiler, private fc: FacadeConverter) { super(tr); }
89

910
visitNode(node: ts.Node): boolean {
1011
switch (node.kind) {
@@ -26,21 +27,11 @@ class CallTranspiler extends base.TranspilerStep {
2627
}
2728
this.visitCall(<ts.NewExpression>node);
2829
break;
29-
case ts.SyntaxKind.ArrayLiteralExpression:
30-
if (this.isInsideConstExpr(node)) {
31-
this.emit('const');
32-
}
33-
return false;
34-
case ts.SyntaxKind.ObjectLiteralExpression:
35-
if (this.isInsideConstExpr(node)) {
36-
this.emit('const');
37-
}
38-
return false;
3930
case ts.SyntaxKind.CallExpression:
4031
var callExpr = <ts.CallExpression>node;
41-
if (!this.maybeHandleSuperCall(callExpr)) {
42-
this.visitCall(callExpr);
43-
}
32+
if (this.fc.maybeHandleCall(callExpr)) break;
33+
if (this.maybeHandleSuperCall(callExpr)) break;
34+
this.visitCall(callExpr);
4435
break;
4536
case ts.SyntaxKind.SuperKeyword:
4637
this.emit('super');
@@ -52,30 +43,6 @@ class CallTranspiler extends base.TranspilerStep {
5243
}
5344

5445
private visitCall(c: ts.CallExpression) {
55-
if (this.isConstCall(c)) {
56-
// The special function CONST_EXPR translates to Dart's const expressions.
57-
if (c.arguments.length !== 1) this.reportError(c, 'CONST_EXPR takes exactly one argument');
58-
this.visitList(c.arguments);
59-
return;
60-
}
61-
62-
if (this.isForwardRef(c)) {
63-
// The special function FORWARD_REF translated to an unwrapped value
64-
if (c.arguments.length !== 1) {
65-
this.reportError(c, 'FORWARD_REF takes exactly one argument');
66-
return;
67-
}
68-
69-
const callback = <ts.FunctionExpression>c.arguments[0];
70-
if (callback.kind !== ts.SyntaxKind.ArrowFunction) {
71-
this.reportError(c, 'FORWARD_REF takes only arrow functions');
72-
return;
73-
}
74-
75-
this.visit(callback.body);
76-
return;
77-
}
78-
7946
this.visit(c.expression);
8047
this.emit('(');
8148
if (!this.handleNamedParamsCall(c)) {

lib/declaration.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import ts = require('typescript');
33
import base = require('./base');
44
import ts2dart = require('./main');
55

6-
class DeclarationTranspiler extends base.TranspilerStep {
6+
class DeclarationTranspiler extends base.TranspilerBase {
77
constructor(tr: ts2dart.Transpiler) { super(tr); }
88

99
visitNode(node: ts.Node): boolean {

lib/expression.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import ts = require('typescript');
33
import base = require('./base');
44
import ts2dart = require('./main');
5+
import {FacadeConverter} from './facade_converter';
56

6-
class ExpressionTranspiler extends base.TranspilerStep {
7-
constructor(tr: ts2dart.Transpiler) { super(tr); }
7+
class ExpressionTranspiler extends base.TranspilerBase {
8+
constructor(tr: ts2dart.Transpiler, private fc: FacadeConverter) { super(tr); }
89

910
visitNode(node: ts.Node): boolean {
1011
switch (node.kind) {
@@ -72,6 +73,7 @@ class ExpressionTranspiler extends base.TranspilerStep {
7273
this.hasAncestor(propAccess, ts.SyntaxKind.CatchClause)) {
7374
this.emitNoSpace('_stack');
7475
} else {
76+
this.fc.checkPropertyAccess(propAccess);
7577
this.emit('.');
7678
this.visit(propAccess.name);
7779
}

lib/facade_converter.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/// <reference path='../node_modules/typescript/bin/typescript.d.ts' />
2+
import base = require('./base');
3+
import ts = require('typescript');
4+
import ts2dart = require('./main');
5+
6+
type FacadeHandler = (c: ts.CallExpression, context: ts.Expression) => void;
7+
8+
export class FacadeConverter extends base.TranspilerBase {
9+
private tc: ts.TypeChecker;
10+
private forbiddenNames: {[fileName: string]: boolean};
11+
12+
constructor(transpiler: ts2dart.Transpiler) {
13+
super(transpiler);
14+
this.forbiddenNames = {};
15+
for (var fileName in this.subs) {
16+
Object.keys(this.subs[fileName])
17+
.map((fnName) => fnName.substring(fnName.lastIndexOf('.') + 1))
18+
.forEach((fnName) => this.forbiddenNames[fnName] = true);
19+
}
20+
}
21+
22+
setTypeChecker(tc: ts.TypeChecker) { this.tc = tc; }
23+
24+
maybeHandleCall(c: ts.CallExpression): boolean {
25+
if (!this.tc) return false;
26+
27+
var symbol: ts.Symbol;
28+
var context: ts.Expression;
29+
30+
if (c.expression.kind === ts.SyntaxKind.Identifier) {
31+
// Function call.
32+
symbol = this.tc.getSymbolAtLocation(c.expression);
33+
context = null;
34+
} else if (c.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
35+
// Method call.
36+
var pa = <ts.PropertyAccessExpression>c.expression;
37+
symbol = this.tc.getSymbolAtLocation(pa.name);
38+
context = pa.expression;
39+
} else {
40+
// Not a call we recognize.
41+
return false;
42+
}
43+
44+
if (!symbol) {
45+
var ident = base.ident(c.expression);
46+
if (ident && this.forbiddenNames[ident]) this.reportMissingType(c, ident);
47+
return false;
48+
}
49+
50+
if (symbol.flags & ts.SymbolFlags.Alias) symbol = this.tc.getAliasedSymbol(symbol);
51+
if (!symbol.valueDeclaration) return false;
52+
53+
var fileName = symbol.valueDeclaration.getSourceFile().fileName;
54+
fileName = fileName.replace(/(\.d)?\.ts$/, '');
55+
56+
// console.log('fn:', fileName);
57+
var fileSubs = this.subs[fileName];
58+
var qn = this.tc.getFullyQualifiedName(symbol);
59+
// Function Qualified Names include their file name. Might be a bug in TypeScript, for the
60+
// time being just special case.
61+
if (symbol.flags & ts.SymbolFlags.Function) qn = symbol.getName();
62+
63+
// console.log('qn', qn);
64+
65+
var qnSub = fileSubs[qn];
66+
if (!qnSub) return false;
67+
68+
qnSub(c, context);
69+
return true;
70+
}
71+
72+
private subs: ts.Map<ts.Map<FacadeHandler>> = {
73+
'lib': {
74+
'Array.push': (c: ts.CallExpression, context: ts.Expression) => {
75+
this.visit(context);
76+
this.emitCall('add', c.arguments);
77+
},
78+
'Array.map': (c: ts.CallExpression, context: ts.Expression) => {
79+
this.visit(context);
80+
this.emitCall('map', c.arguments);
81+
this.emitCall('toList');
82+
},
83+
'Array.forEach': (c: ts.CallExpression, context: ts.Expression) => {
84+
this.visit(context);
85+
this.emitCall('forEach', c.arguments);
86+
this.emitCall('toList');
87+
},
88+
'Array.slice': (c: ts.CallExpression, context: ts.Expression) => {
89+
this.emitCall('ListWrapper.slice', [context, ...c.arguments]);
90+
},
91+
'Array.splice': (c: ts.CallExpression, context: ts.Expression) => {
92+
this.emitCall('ListWrapper.splice', [context, ...c.arguments]);
93+
},
94+
'Array.concat': (c: ts.CallExpression, context: ts.Expression) => {
95+
this.emit('new List . from (');
96+
this.visit(context);
97+
this.emit(') .. addAll (');
98+
this.visit(c.arguments[0]);
99+
this.emit(')');
100+
},
101+
'Array.isArray': (c: ts.CallExpression, context: ts.Expression) => {
102+
this.visit(context);
103+
this.emit('is List');
104+
},
105+
},
106+
'angular2/traceur-runtime': {
107+
'Map.set': (c: ts.CallExpression, context: ts.Expression) => {
108+
this.visit(context);
109+
this.emit('[');
110+
this.visit(c.arguments[0]);
111+
this.emit(']');
112+
this.emit('=');
113+
this.visit(c.arguments[1]);
114+
},
115+
'Map.get': (c: ts.CallExpression, context: ts.Expression) => {
116+
this.visit(context);
117+
this.emit('[');
118+
this.visit(c.arguments[0]);
119+
this.emit(']');
120+
},
121+
},
122+
'angular2/src/facade/lang': {
123+
'CONST_EXPR': (c: ts.CallExpression, context: ts.Expression) => {
124+
// `const` keyword is emitted in the array literal handling, as it needs to be transitive.
125+
this.visitList(c.arguments);
126+
},
127+
'FORWARD_REF': (c: ts.CallExpression, context: ts.Expression) => {
128+
// The special function FORWARD_REF translates to an unwrapped value in Dart.
129+
const callback = <ts.FunctionExpression>c.arguments[0];
130+
if (callback.kind !== ts.SyntaxKind.ArrowFunction) {
131+
this.reportError(c, 'FORWARD_REF takes only arrow functions');
132+
return;
133+
}
134+
this.visit(callback.body);
135+
}
136+
},
137+
};
138+
139+
private emitCall(name: string, args?: ts.Expression[]) {
140+
this.emit('.');
141+
this.emit(name);
142+
this.emit('(');
143+
if (args) this.visitList(args);
144+
this.emit(')');
145+
}
146+
147+
checkPropertyAccess(pa: ts.PropertyAccessExpression) {
148+
if (!this.tc) return;
149+
var ident = pa.name.text;
150+
if (this.forbiddenNames[ident] && !this.tc.getSymbolAtLocation(pa.name)) {
151+
this.reportMissingType(pa, ident);
152+
}
153+
}
154+
155+
reportMissingType(n: ts.Node, ident: string) {
156+
this.reportError(n, `Untyped property access to "${ident}" which could be special.\n` +
157+
` Please add type declarations to disambiguate.`);
158+
}
159+
160+
isInsideConstExpr(node: ts.Node): boolean {
161+
return this.isConstCall(
162+
<ts.CallExpression>this.getAncestor(node, ts.SyntaxKind.CallExpression));
163+
}
164+
165+
private isConstCall(node: ts.CallExpression): boolean {
166+
return node && base.ident(node.expression) === 'CONST_EXPR';
167+
}
168+
}

lib/literal.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import ts = require('typescript');
33
import base = require('./base');
44
import ts2dart = require('./main');
5+
import {FacadeConverter} from "./facade_converter";
56

6-
class LiteralTranspiler extends base.TranspilerStep {
7-
constructor(tr: ts2dart.Transpiler) { super(tr); }
7+
class LiteralTranspiler extends base.TranspilerBase {
8+
constructor(tr: ts2dart.Transpiler, private fc: FacadeConverter) { super(tr); }
89

910
visitNode(node: ts.Node): boolean {
1011
switch (node.kind) {
@@ -49,13 +50,13 @@ class LiteralTranspiler extends base.TranspilerStep {
4950
if (span.literal) this.visit(span.literal);
5051
break;
5152
case ts.SyntaxKind.ArrayLiteralExpression:
52-
if (this.hasAncestor(node, ts.SyntaxKind.Decorator)) this.emit('const');
53+
if (this.shouldBeConst(node)) this.emit('const');
5354
this.emit('[');
5455
this.visitList((<ts.ArrayLiteralExpression>node).elements);
5556
this.emit(']');
5657
break;
5758
case ts.SyntaxKind.ObjectLiteralExpression:
58-
if (this.hasAncestor(node, ts.SyntaxKind.Decorator)) this.emit('const');
59+
if (this.shouldBeConst(node)) this.emit('const');
5960
this.emit('{');
6061
this.visitList((<ts.ObjectLiteralExpression>node).properties);
6162
this.emit('}');
@@ -104,6 +105,10 @@ class LiteralTranspiler extends base.TranspilerStep {
104105
return true;
105106
}
106107

108+
private shouldBeConst(n: ts.Node): boolean {
109+
return this.hasAncestor(n, ts.SyntaxKind.Decorator) || this.fc.isInsideConstExpr(n);
110+
}
111+
107112
private escapeTextForTemplateString(n: ts.Node): string {
108113
return (<ts.StringLiteral>n).text.replace(/\\/g, '\\\\').replace(/([$'])/g, '\\$1');
109114
}

0 commit comments

Comments
 (0)