Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Support Fields #2

Merged
merged 3 commits into from
Nov 5, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,10 @@ gulp.task('test.e2e', ['test.compile'], function(done) {

gulp.task('test', ['test.unit', 'test.e2e', 'test.check-format']);

gulp.task('watch', ['test.unit', 'test.check-format'], function() {
gulp.task('watch', function() {
failOnError = false;
// Avoid watching generated .d.ts in the build (aka output) directory.
return gulp.watch(
['src/**/*.ts', 'test/**/*.ts', 'test_files/**'], {ignoreInitial: true}, ['test.unit']);
gulp.start(['test.unit']); // Trigger initial build.
return gulp.watch(['src/**/*.ts', 'test/**/*.ts', 'test_files/**'], ['test.unit']);
});

gulp.task('default', ['compile']);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"devDependencies": {
"chai": "^2.1.1",
"clang-format": "^1.0.32",
"clang-format": "1.0.33",
"closure-compiler": "^0.2.12",
"gulp": "^3.8.11",
"gulp-clang-format": "^1.0.22",
Expand Down
114 changes: 95 additions & 19 deletions src/sickle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import * as ts from 'typescript';
export function formatDiagnostics(diags: ts.Diagnostic[]): string {
return diags.map((d) => {
let res = ts.DiagnosticCategory[d.category];
if (d.file) res += d.file + ':';
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;
res += ' ' + d.messageText;
return res;
})
.join('\n');
Expand All @@ -20,6 +20,8 @@ export type StringMap = {

export type AnnotatedProgram = StringMap;

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.
Expand Down Expand Up @@ -51,35 +53,80 @@ class Annotator {
return res;
}

private emit(str: string) { this.output.push(str); }

private visit(node: ts.Node) {
// console.log('node:', (<any>ts).SyntaxKind[node.kind]);
switch (node.kind) {
case ts.SyntaxKind.VariableDeclaration:
this.maybeVisitType((<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;
}
// Emit a synthetic ctor.
// TODO(martinprobst): Handle inherited parent ctors.
this.writeTextBetween(classNode, classNode.getLastToken());
this.emit('constructor() {\n');
this.emitStubDeclarations(classNode, []);
this.emit('}\n');
this.writeNode(classNode.getLastToken());
break;
}
case ts.SyntaxKind.Constructor: {
let ctor = <ts.ConstructorDeclaration>node;
this.writeTextBetween(ctor, ctor.body);
if (ctor.body.statements.length) {
// Insert before the first code in the ctor.
this.writeTextBetween(ctor.body, ctor.body.statements[0]);
} else {
// Empty ctor - just insert before the end of it.
this.writeTextBetween(ctor.body, ctor.body.getLastToken());
}

let paramProps = ctor.parameters.filter((p) => !!(p.flags & VISIBILITY_FLAGS));
this.emitStubDeclarations(<ts.ClassLikeDeclaration>ctor.parent, paramProps);

if (ctor.body.statements.length) {
let firstStmt = ctor.body.statements[0];
this.emit(
ctor.body.getSourceFile().getText().substring(
firstStmt.getFullStart(), ctor.body.getEnd()));
} else {
let remaining = ctor.getSourceFile().getText().substring(
ctor.body.getLastToken().getFullStart(), ctor.body.getEnd());
this.emit(remaining);
}

break;
}
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.ArrowFunction:
let fnDecl = <ts.FunctionLikeDeclaration>node;
this.maybeVisitType(fnDecl.type, true);
this.maybeVisitType(fnDecl.type, '@return');
let writeOffset = fnDecl.getFullStart();
// Parameters.
if (fnDecl.parameters.length) {
for (let param of fnDecl.parameters) {
this.writeTextBetween(fnDecl, writeOffset, param);
this.writeTextFromOffset(writeOffset, param);
writeOffset = param.getEnd();
this.maybeVisitType(param.type);
this.visit(param);
}
}
// Return type.
if (fnDecl.type) {
this.writeTextBetween(fnDecl, writeOffset, fnDecl.type);
this.writeTextFromOffset(writeOffset, fnDecl.type);
this.visit(fnDecl.type);
writeOffset = fnDecl.type.getEnd();
}
// Body.
this.writeTextBetween(fnDecl, writeOffset, fnDecl.body);
this.writeTextFromOffset(writeOffset, fnDecl.body);
this.visit(fnDecl.body);
break;
default:
Expand All @@ -88,23 +135,44 @@ class Annotator {
}
}

private maybeVisitType(type: ts.TypeNode, isReturn?: boolean) {
private emitStubDeclarations(
classDecl: ts.ClassLikeDeclaration, paramProps: ts.ParameterDeclaration[]) {
this.emit('\n\n// Sickle: begin stub declarations.\n');
this.emit('\n');
let props = <ts.PropertyDeclaration[]>(
classDecl.members.filter((e) => e.kind === ts.SyntaxKind.PropertyDeclaration));
props.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) {
this.maybeVisitType(p.type, '@type');
this.emit('\nthis.');
this.emit(p.name.getText());
this.emit(';');
this.emit('\n');
}

private maybeVisitType(type: ts.TypeNode, jsDocTag?: string) {
if (!type) return;
this.output.push(' /**');
if (isReturn) {
this.output.push(' @return {');
this.emit(' /**');
if (jsDocTag) {
this.emit(' ');
this.emit(jsDocTag);
this.emit(' {');
}
this.visit(type);
if (isReturn) {
this.output.push('}');
if (jsDocTag) {
this.emit('}');
}
this.output.push(' */');
this.emit(' */');
}

private writeNode(node: ts.Node, upTo?: number) {
private writeNode(node: ts.Node) {
if (node.getChildCount() == 0) {
// Directly write complete tokens.
this.output.push(node.getFullText());
this.emit(node.getFullText());
return;
}
let lastEnd = node.getFullStart();
Expand All @@ -114,17 +182,25 @@ class Annotator {
this.visit(child);
lastEnd = child.getEnd();
}
// Write any trailing text.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, Daria had exactly the same bug, in the basically the same code. I should have caught it in the first PR pass.

let text = node.getSourceFile().getText().slice(lastEnd, node.getEnd());
if (text) this.emit(text);
}

private writeTextBetween(node: ts.Node, to: ts.Node) {
let text = node.getSourceFile().getText().slice(node.getFullStart(), to.getFullStart());
if (text) this.emit(text);
}

private writeTextBetween(node: ts.Node, from: number, to: ts.Node) {
let text = node.getSourceFile().getText().slice(from, to.getFullStart());
this.output.push(text);
private writeTextFromOffset(from: number, to: ts.Node) {
let text = to.getSourceFile().getText().slice(from, to.getFullStart());
if (text) this.emit(text);
}

private writeSourceBefore(offset: number, node: ts.Node) {
if (node.getFullStart() == offset) return;
assert(node.getFullStart() > offset, 'Offset must not be smaller');
this.output.push(node.getSourceFile().getText().slice(offset, node.getFullStart()));
this.emit(node.getSourceFile().getText().slice(offset, node.getFullStart()));
}

private fail(msg: string) { throw new Error(msg); }
Expand Down
2 changes: 1 addition & 1 deletion test/e2e_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function checkClosureCompile(jsFiles: string[], done: (err: Error) => voi
'checks-only': true,
'jscomp_error': 'checkTypes',
'js': jsFiles,
'language_in': 'ECMASCRIPT6'
'language_in': 'ECMASCRIPT6_STRICT'
};

compile(null, CLOSURE_COMPILER_OPTS, (err, stdout, stderr) => {
Expand Down
8 changes: 8 additions & 0 deletions test/sickle_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import {expect} from 'chai';
import {annotateProgram, formatDiagnostics} from '../src/sickle';
import {expectSource, goldenTests} from './test_support';

let RUN_TESTS_MATCHING: RegExp = null;
// RUN_TESTS_MATCHING = /fields/;

describe('golden tests', () => {

goldenTests().forEach((test) => {
if (RUN_TESTS_MATCHING && !RUN_TESTS_MATCHING.exec(test.name)) {
it.skip(test.name);
return;
}
var tsSource = fs.readFileSync(test.tsPath, 'utf-8');
var jsSource = fs.readFileSync(test.jsPath, 'utf-8');
it(test.name, () => { expectSource(tsSource).to.equal(jsSource); });
Expand Down
5 changes: 3 additions & 2 deletions test/test_support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ function transformSource(src: string): string {

var program = ts.createProgram(['main.ts'], OPTIONS, host);
if (program.getSyntacticDiagnostics().length) {
throw new Error(formatDiagnostics(ts.getPreEmitDiagnostics(program)));
throw new Error(
'Failed to parse ' + src + '\n' + formatDiagnostics(ts.getPreEmitDiagnostics(program)));
}

var transformed: StringMap = {};
Expand All @@ -73,7 +74,7 @@ function transformSource(src: string): string {

export function expectSource(src: string) {
var annotated = annotateSource(src);
// console.log('Annotated', annotated);
// console.log('Annotated:\n', annotated);
var transformed = transformSource(annotated);
return expect(transformed);
}
Expand Down
18 changes: 18 additions & 0 deletions test_files/fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Klass {
constructor(field3) {
// Sickle: begin stub declarations.
this.field3 = field3;
/** @type { string} */
this.field1;
/** @type { number} */
this.field2;
/** @type { number} */
this.field3;
// Sickle: end stub declarations.
this.field3 = 2 + 1;
}
getF1() {
// This access print a warning without a generated field stub declaration.
return this.field1;
}
}
13 changes: 13 additions & 0 deletions test_files/fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Klass {
field1: string;
field2: number;

constructor(private field3: number) {
this.field3 = 2 + 1;
}

getF1() {
// This access print a warning without a generated field stub declaration.
return this.field1;
}
}
8 changes: 8 additions & 0 deletions test_files/fields_no_ctor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class NoCtor {
constructor() {
// Sickle: begin stub declarations.
/** @type { number} */
this.field1;
// Sickle: end stub declarations.
}
}
3 changes: 3 additions & 0 deletions test_files/fields_no_ctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class NoCtor {
field1: number;
}