This repository was archived by the owner on May 22, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 105
Expand file tree
/
Copy pathsickle.ts
More file actions
384 lines (351 loc) · 13 KB
/
sickle.ts
File metadata and controls
384 lines (351 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
import * as ts from 'typescript';
export interface SickleOptions {
// If true, convert every type to the Closure {?} type, which means
// "don't check types".
untyped?: boolean;
}
export function formatDiagnostics(diags: ts.Diagnostic[]): string {
return diags.map((d) => {
let res = ts.DiagnosticCategory[d.category];
if (d.file) res += ' at ' + d.file.fileName + ':';
if (d.start) {
let {line, character} = d.file.getLineAndCharacterOfPosition(d.start);
res += line + ':' + character + ':';
}
res += ' ' + d.messageText;
return res;
})
.join('\n');
}
/**
* Returns true if a class declaration has a superclass (as declared
* with the "extends" keyword), which indicates it must call super()
* in its constructor.
*/
function classHasSuperClass(classNode: ts.ClassDeclaration): boolean {
if (classNode.heritageClauses) {
for (let heritage of classNode.heritageClauses) {
if (heritage.token == ts.SyntaxKind.ExtendsKeyword) {
return true;
}
}
}
return false;
}
const VISIBILITY_FLAGS = ts.NodeFlags.Private | ts.NodeFlags.Protected | ts.NodeFlags.Public;
/**
* A source processor that takes TypeScript code and annotates the output with Closure-style JSDoc
* comments.
*/
class Annotator {
private output: string[];
private indent: number;
private file: ts.SourceFile;
// The node currently being visited by visit().
// This is only used in error messages.
private currentNode: ts.Node;
constructor(private options: SickleOptions) { this.indent = 0; }
annotate(file: ts.SourceFile): string {
this.output = [];
this.file = file;
this.visit(file);
this.assert(this.indent == 0, 'visit() failed to track nesting');
return this.output.join('');
}
private emit(str: string) { this.output.push(str); }
private logWithIndent(message: string) {
let prefix = new Array(this.indent + 1).join('| ');
console.log(prefix + message);
}
private visit(node: ts.Node) {
this.currentNode = node;
// this.logWithIndent('node: ' + (<any>ts).SyntaxKind[node.kind]);
this.indent++;
switch (node.kind) {
case ts.SyntaxKind.VariableDeclaration:
this.maybeEmitJSDocType((<ts.VariableDeclaration>node).type);
this.writeNode(node);
break;
case ts.SyntaxKind.ClassDeclaration: {
let classNode = <ts.ClassDeclaration>node;
let hasCtor = classNode.members.some((e) => e.kind === ts.SyntaxKind.Constructor);
if (hasCtor) {
this.writeNode(classNode);
break;
}
this.writeTextBetween(classNode, classNode.getLastToken());
this.emitSyntheticConstructor(classNode);
this.writeNode(classNode.getLastToken());
break;
}
case ts.SyntaxKind.PublicKeyword:
case ts.SyntaxKind.PrivateKeyword:
// The "public"/"private" keywords are encountered in two places:
// 1) In class fields (which don't appear in the transformed output).
// 2) In "parameter properties", e.g.
// constructor(/** @export */ public foo: string).
// In case 2 it's important to not emit that JSDoc in the generated
// constructor, as this is illegal for Closure. It's safe to just
// always skip comments preceding the 'public' keyword.
// See test_files/parameter_properties.ts.
this.writeNode(node, /* skipComments */ true);
break;
case ts.SyntaxKind.Constructor: {
let ctor = <ts.ConstructorDeclaration>node;
// Write the "constructor(...) {" bit, but iterate through any
// parameters if given so that we can examine them more closely.
let offset = ctor.getFullStart();
if (ctor.parameters.length) {
for (let param of ctor.parameters) {
this.writeTextFromOffset(offset, param);
this.visit(param);
offset = param.getEnd();
}
}
// Write all of the body up to the closing }.
offset = this.writeTextFromOffset(offset, ctor.body.getLastToken());
let paramProps = ctor.parameters.filter((p) => !!(p.flags & VISIBILITY_FLAGS));
this.emitStubDeclarations(<ts.ClassLikeDeclaration>ctor.parent, paramProps);
this.writeRange(offset, ctor.body.getEnd());
break;
}
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.ArrowFunction:
let fnDecl = <ts.FunctionLikeDeclaration>node;
let writeOffset = fnDecl.getFullStart();
writeOffset = this.writeRange(writeOffset, fnDecl.getStart());
// The first \n makes the output sometimes uglier than necessary,
// but it's needed to work around
// https://github.com/Microsoft/TypeScript/issues/6982
this.emit('\n/**\n');
// Parameters.
if (fnDecl.parameters.length) {
for (let param of fnDecl.parameters) {
if (param.type) {
let optional = param.initializer != null || param.questionToken != null;
this.emit(' * @param {');
this.emitType(param.type, optional);
this.emit('} ');
this.writeNode(param.name);
this.emit('\n');
}
}
}
// Return type.
if (fnDecl.type) {
this.emit(' * @return {');
this.emitType(fnDecl.type);
this.emit('}\n');
}
this.emit(' */\n');
this.writeTextFromOffset(writeOffset, fnDecl.body);
this.visit(fnDecl.body);
break;
case ts.SyntaxKind.TypeAliasDeclaration:
this.visitTypeAlias(<ts.TypeAliasDeclaration>node);
this.writeNode(node);
break;
case ts.SyntaxKind.EnumDeclaration:
this.visitEnum(<ts.EnumDeclaration>node);
break;
default:
this.writeNode(node);
break;
}
this.indent--;
}
// emitSyntheticConstructor produces a constructor() {...} where
// none existed in the original source. It's necessary in the case
// where TypeScript syntax specifies there are additional properties
// on the class, because to declare these in Closure you must put
// those in the constructor.
private emitSyntheticConstructor(classNode: ts.ClassDeclaration) {
// Be careful to emit minimal code here, as fully implementing a
// constructor is hard. See test_files/super.ts for some test cases.
if (classNode.members.length == 0) {
// There are no members so we can rely on the default TypeScript
// constructor.
return;
}
this.emit('\n// Sickle: begin synthetic ctor.\n');
this.emit('constructor() {\n');
if (classHasSuperClass(classNode)) {
// We must call super(), but we don't know the necessary arguments.
// For now, just assume there are none, as that covers many cases.
this.emit('super();\n');
}
this.emitStubDeclarations(classNode, []);
this.emit('}\n');
}
private emitStubDeclarations(
classDecl: ts.ClassLikeDeclaration, paramProps: ts.ParameterDeclaration[]) {
this.emit('\n\n// Sickle: begin stub declarations.\n');
this.emit('\n');
let nonStaticProps = <ts.PropertyDeclaration[]>(classDecl.members.filter((e) => {
let isStatic = (e.flags & ts.NodeFlags.Static) !== 0;
let isProperty = e.kind === ts.SyntaxKind.PropertyDeclaration;
return !isStatic && isProperty;
}));
nonStaticProps.forEach((p) => this.visitProperty(p));
paramProps.forEach((p) => this.visitProperty(p));
this.emit('// Sickle: end stub declarations.\n');
}
private visitProperty(p: ts.PropertyDeclaration | ts.ParameterDeclaration) {
let existingAnnotation = this.existingClosureAnnotation(p).trim();
if (existingAnnotation) {
existingAnnotation += '\n';
}
this.maybeEmitJSDocType(p.type, existingAnnotation + '@type');
this.emit('\nthis.');
this.emit(p.name.getText());
this.emit(';');
this.emit('\n');
}
/**
* Returns empty string if there is no existing annotation.
*/
private existingClosureAnnotation(p: ts.PropertyDeclaration | ts.ParameterDeclaration) {
let text = p.getFullText();
let comments = ts.getLeadingCommentRanges(text, 0);
if (!comments || comments.length == 0) return '';
// JS compiler only considers the last comment significant.
let {pos, end} = comments[comments.length - 1];
let comment = text.substring(pos, end);
return Annotator.getJsDocAnnotation(comment);
}
// return empty string if comment is not JsDoc.
static getJsDocAnnotation(comment: string): string {
if (/^\/\*\*/.test(comment) && /\*\/$/.test(comment)) {
return comment.slice(3, comment.length - 2);
}
return '';
}
private maybeEmitJSDocType(type: ts.TypeNode, jsDocTag?: string) {
if (!type && !this.options.untyped) return;
this.emit(' /**');
if (jsDocTag) {
this.emit(' ');
this.emit(jsDocTag);
this.emit(' {');
}
this.emitType(type);
if (jsDocTag) {
this.emit('}');
}
this.emit(' */');
}
private emitType(type: ts.TypeNode, optional?: boolean) {
if (this.options.untyped) {
this.emit(' ?');
} else {
this.visit(type);
}
if (optional) {
this.emit('=');
}
}
private visitTypeAlias(node: ts.TypeAliasDeclaration) {
if (this.options.untyped) return;
// Write a Closure typedef, which involves an unused "var" declaration.
this.emit('/** @typedef {');
this.visit(node.type);
this.emit('} */\n');
this.emit('var ');
this.emit(node.name.getText());
this.emit(': void;\n');
}
private visitEnum(node: ts.EnumDeclaration) {
this.emit('/** @typedef {number} */\n');
this.writeNode(node);
this.emit('\n');
let i = 0;
for (let member of node.members) {
this.emit(`/** @type {${node.name.getText()}} */\n`);
this.emit(`(<any>${node.name.getText()}).${member.name.getText()} = `);
if (member.initializer) {
this.visit(member.initializer);
} else {
this.emit(String(i));
i++;
}
this.emit(';\n');
}
}
private writeNode(node: ts.Node, skipComments: boolean = false) {
if (node.getChildCount() == 0) {
// Directly write complete tokens.
if (skipComments) {
// To skip comments, we skip all whitespace/comments preceding
// the node. But if there was anything skipped we should emit
// a newline in its place so that the node remains separated
// from the previous node. TODO: don't skip anything here if
// there wasn't any comment.
if (node.getFullStart() < node.getStart()) {
this.emit('\n');
}
this.emit(node.getText());
} else {
this.emit(node.getFullText());
}
return;
}
if (skipComments) {
this.fail('skipComments unimplemented for complex Nodes');
}
let lastEnd = node.getFullStart();
for (let child of node.getChildren()) {
this.writeTextFromOffset(lastEnd, child);
this.visit(child);
lastEnd = child.getEnd();
}
// Write any trailing text.
this.writeRange(lastEnd, node.getEnd());
}
// Write a span of the input file as expressed by absolute offsets.
// These offsets are found in attributes like node.getFullStart() and
// node.getEnd().
private writeRange(from: number, to: number): number {
// getSourceFile().getText() is wrong here because it the text of
// the SourceFile node of the AST, which doesn't contain the comments
// preceding that node. Semantically these ranges are just offsets
// into the original source file text, so slice from that.
let text = this.file.text.slice(from, to);
if (text) this.emit(text);
return to;
}
private writeTextBetween(node: ts.Node, to: ts.Node): number {
return this.writeRange(node.getFullStart(), to.getFullStart());
}
private writeTextFromOffset(from: number, node: ts.Node): number {
let to = node.getFullStart();
if (from == to) return to;
this.assert(to > from, `Offset must not be smaller; ${to} vs ${from}`);
return this.writeRange(from, to);
}
private fail(msg: string) {
let offset = this.currentNode.getFullStart();
let {line, character} = this.file.getLineAndCharacterOfPosition(offset);
throw new Error(`near node starting at ${line+1}:${character+1}: ${msg}`);
}
private assert(condition: boolean, msg: string) {
if (!condition) this.fail(msg);
}
}
function last<T>(elems: T[]): T {
return elems.length ? elems[elems.length - 1] : null;
}
export function annotate(file: ts.SourceFile, options: SickleOptions = {}): string {
let fullOptions: SickleOptions = {
untyped: options.untyped || false,
};
return new Annotator(fullOptions).annotate(file);
}
// CLI entry point
if (require.main === module) {
for (let path of process.argv.splice(2)) {
let sourceText = ts.sys.readFile(path, 'utf-8');
let sf = ts.createSourceFile(path, sourceText, ts.ScriptTarget.ES6, true);
console.log(path + ':');
console.log(annotate(sf));
}
}