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

feat: Closure compile, golden file base tests. #1

Merged
merged 8 commits into from
Nov 4, 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
19 changes: 14 additions & 5 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var tsProject = ts.createProject(TSC_OPTIONS);

gulp.task('test.check-format', function() {
return gulp.src(['*.js', 'src/**/*.ts', 'test/**/*.ts'])
.pipe(formatter.checkFormat('file', clangFormat))
.pipe(formatter.checkFormat('file', clangFormat, {verbose: true}))
.on('warning', onError);
});

Expand Down Expand Up @@ -73,15 +73,24 @@ gulp.task('test.unit', ['test.compile'], function(done) {
done();
return;
}
return gulp.src('build/test/**/*.js').pipe(mocha({timeout: 500}));
return gulp.src(['build/test/**/*.js', '!build/test/**/e2e*.js']).pipe(mocha({timeout: 1000}));
});

gulp.task('test', ['test.unit', 'test.check-format']);
gulp.task('test.e2e', ['test.compile'], function(done) {
if (hasError) {
Copy link
Contributor

Choose a reason for hiding this comment

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

interesting pattern, there has to be a better way to compose errors in tasks :) (no action required)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, gulp has no systematic way to deal with errors, and surprisingly it's still better than all the other tools in that regard...

done();
return;
}
return gulp.src(['build/test/**/e2e*.js']).pipe(mocha({timeout: 10000}));
});

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

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

gulp.task('default', ['compile']);
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
"dependencies": {
"source-map": "^0.4.2",
"source-map-support": "^0.3.1",
"typescript": "Microsoft/TypeScript"
"typescript": "^1.6.2"
},
"devDependencies": {
"chai": "^2.1.1",
"clang-format": "1.0.30",
"clang-format": "^1.0.32",
"closure-compiler": "^0.2.12",
"gulp": "^3.8.11",
"gulp-clang-format": "^1.0.21",
"gulp-clang-format": "^1.0.22",
"gulp-mocha": "^2.0.0",
"gulp-sourcemaps": "^1.5.0",
"gulp-typescript": "^2.7.6",
Expand Down
16 changes: 9 additions & 7 deletions src/sickle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export function formatDiagnostics(diags: ts.Diagnostic[]): string {
.join('\n');
}

export type AnnotatedProgram = {
export type StringMap = {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe no need to export this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

error TS4081: Exported type alias 'AnnotatedProgram' has or is using private name 'StringMap'.

Which makes sense, I guess?

[fileName: string]: string
};

export type AnnotatedProgram = StringMap;

/**
* A source processor that takes TypeScript code and annotates the output with Closure-style JSDoc
* comments.
Expand All @@ -27,18 +29,18 @@ class Annotator {

constructor() {}

transform(args: string[]): AnnotatedProgram {
annotate(args: string[]): AnnotatedProgram {
let tsArgs = ts.parseCommandLine(args);
if (tsArgs.errors) {
this.fail(formatDiagnostics(tsArgs.errors));
}
let program = ts.createProgram(tsArgs.fileNames, tsArgs.options);
let diags = ts.getPreEmitDiagnostics(program);
if (diags && diags.length) this.fail(formatDiagnostics(diags));
return this.transformProgram(program);
return this.annotateProgram(program);
}

transformProgram(program: ts.Program): AnnotatedProgram {
annotateProgram(program: ts.Program): AnnotatedProgram {
let res: AnnotatedProgram = {};
for (let sf of program.getSourceFiles()) {
if (sf.fileName.match(/\.d\.ts$/)) continue;
Expand Down Expand Up @@ -136,13 +138,13 @@ function last<T>(elems: T[]): T {
return elems.length ? elems[elems.length - 1] : null;
}

export function transformProgram(program: ts.Program): AnnotatedProgram {
return new Annotator().transformProgram(program);
export function annotateProgram(program: ts.Program): AnnotatedProgram {
return new Annotator().annotateProgram(program);
}

// CLI entry point
if (require.main === module) {
let res = new Annotator().transform(process.argv);
let res = new Annotator().annotate(process.argv);
// TODO(martinprobst): Do something useful here...
console.log(JSON.stringify(res));
}
5 changes: 5 additions & 0 deletions test/closure-compiler.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'closure-compiler' {
export interface CompileOptions { [k: string]: boolean | string | string[]; }
type Callback = (err: Error, stdout: string, stderr: string) => void;
function compile(src: string, options?: CompileOptions | Callback, callback?: Callback): void;
}
33 changes: 33 additions & 0 deletions test/e2e_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {expect} from 'chai';
import {CompileOptions, compile} from 'closure-compiler';

import {annotateProgram, formatDiagnostics} from '../src/sickle';
import {goldenTests} from './test_support';

export function checkClosureCompile(jsFiles: string[], done: (err: Error) => void) {
var startTime = Date.now();
var total = jsFiles.length;
if (!total) throw new Error('No JS files in ' + JSON.stringify(jsFiles));

var CLOSURE_COMPILER_OPTS: CompileOptions = {
'checks-only': true,
'jscomp_error': 'checkTypes',
'js': jsFiles,
'language_in': 'ECMASCRIPT6'
};

compile(null, CLOSURE_COMPILER_OPTS, (err, stdout, stderr) => {
console.log('Closure compilation:', total, 'done after', Date.now() - startTime, 'ms');
done(err);
});
}

describe('golden file tests', () => {
it('generates correct Closure code', (done: (err: Error) => void) => {
var goldenJs = goldenTests().map((t) => t.jsPath);
checkClosureCompile(goldenJs, done);
});
});
69 changes: 10 additions & 59 deletions test/sickle_test.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,15 @@
import {expect} from 'chai';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {expect} from 'chai';

import {transformProgram, formatDiagnostics} from '../src/sickle';

const OPTIONS: ts.CompilerOptions = {
noImplicitAny: true,
noResolve: true,
skipDefaultLibCheck: true,
};

const {cachedLibName, cachedLib} = (function() {
let host = ts.createCompilerHost(OPTIONS);
let fn = host.getDefaultLibFileName(OPTIONS);
return {cachedLibName: fn, cachedLib: host.getSourceFile(fn, ts.ScriptTarget.ES6)};
})();

function transformSource(src: string): string {
var host = ts.createCompilerHost(OPTIONS);
var original = host.getSourceFile.bind(host);
host.getSourceFile = function(fileName: string, languageVersion: ts.ScriptTarget,
onError?: (msg: string) => void): ts.SourceFile {
if (fileName === cachedLibName) return cachedLib;
if (fileName === 'main.ts') {
return ts.createSourceFile(fileName, src, ts.ScriptTarget.Latest, true);
}
return original(fileName, languageVersion, onError);
};

var program = ts.createProgram(['main.ts'], {}, host);
if (program.getSyntacticDiagnostics().length) {
throw new Error(formatDiagnostics(ts.getPreEmitDiagnostics(program)));
}

var res = transformProgram(program);
expect(Object.keys(res)).to.deep.equal(['main.ts']);
return res['main.ts'];
}

function expectSource(src: string) {
return expect(transformSource(src));
}
import {annotateProgram, formatDiagnostics} from '../src/sickle';
import {expectSource, goldenTests} from './test_support';

describe('adding JSDoc types', () => {
it('handles variable declarations', () => {
expectSource('var x: string;').to.equal('var /** string */ x: string;');
expectSource('var x: string, y: number;')
.to.equal('var /** string */ x: string, /** number */ y: number;');
});
it('handles function declarations', () => {
expectSource('function x(a: number): string {\n' +
' return "a";\n' +
'}')
.to.equal(' /** @return { string} */function x( /** number */a: number): string {\n' +
' return "a";\n' +
'}');
expectSource('function x(a: number, b: number) {}')
.to.equal('function x( /** number */a: number, /** number */ b: number) {}');
});
it('handles arrow functions', () => {
expectSource('var x = (a: number): number => 12;')
.to.equal('var x = /** @return { number} */ ( /** number */a: number): number => 12;');
describe('golden tests', () => {
goldenTests().forEach((test) => {
var tsSource = fs.readFileSync(test.tsPath, 'utf-8');
var jsSource = fs.readFileSync(test.jsPath, 'utf-8');
it(test.name, () => { expectSource(tsSource).to.equal(jsSource); });
});
});
76 changes: 66 additions & 10 deletions test/test_support.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,98 @@
import {expect} from 'chai';
import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';

import {transformProgram, formatDiagnostics} from '../src/sickle';
import {annotateProgram, formatDiagnostics, StringMap} from '../src/sickle';

const OPTIONS: ts.CompilerOptions = {
target: ts.ScriptTarget.ES6,
noImplicitAny: true,
noResolve: true,
skipDefaultLibCheck: true,
};

const {cachedLibName, cachedLib} = (function() {
const {cachedLibPath, cachedLib} = (function() {
let host = ts.createCompilerHost(OPTIONS);
let fn = host.getDefaultLibFileName(OPTIONS);
return {cachedLibName: fn, cachedLib: host.getSourceFile(fn, ts.ScriptTarget.ES6)};
let p = ts.getDefaultLibFilePath(OPTIONS);
return {cachedLibPath: p, cachedLib: host.getSourceFile(fn, ts.ScriptTarget.ES6)};
})();

function transformSource(src: string): string {
function annotateSource(src: string): string {
var host = ts.createCompilerHost(OPTIONS);
var original = host.getSourceFile.bind(host);
host.getSourceFile = function(fileName: string, languageVersion: ts.ScriptTarget,
onError?: (msg: string) => void): ts.SourceFile {
if (fileName === cachedLibName) return cachedLib;
host.getSourceFile = function(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (msg: string) => void): ts.SourceFile {
if (fileName === cachedLibPath) return cachedLib;
if (fileName === 'main.ts') {
return ts.createSourceFile(fileName, src, ts.ScriptTarget.Latest, true);
}
return original(fileName, languageVersion, onError);
};

var program = ts.createProgram(['main.ts'], {}, host);
var program = ts.createProgram(['main.ts'], OPTIONS, host);
if (program.getSyntacticDiagnostics().length) {
throw new Error(formatDiagnostics(ts.getPreEmitDiagnostics(program)));
}

var res = transformProgram(program);
var res = annotateProgram(program);
expect(Object.keys(res)).to.deep.equal(['main.ts']);
return res['main.ts'];
}

function transformSource(src: string): string {
var host = ts.createCompilerHost(OPTIONS);
var original = host.getSourceFile.bind(host);
var mainSrc = ts.createSourceFile('main.ts', src, ts.ScriptTarget.Latest, true);
host.getSourceFile = function(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (msg: string) => void): ts.SourceFile {
if (fileName === cachedLibPath) return cachedLib;
if (fileName === 'main.ts') {
return mainSrc;
}
return original(fileName, languageVersion, onError);
};

var program = ts.createProgram(['main.ts'], OPTIONS, host);
if (program.getSyntacticDiagnostics().length) {
throw new Error(formatDiagnostics(ts.getPreEmitDiagnostics(program)));
}

var transformed: StringMap = {};
var emitRes =
program.emit(mainSrc, (fileName: string, data: string) => { transformed[fileName] = data; });
if (emitRes.diagnostics.length) {
throw new Error(formatDiagnostics(emitRes.diagnostics));
}
expect(Object.keys(transformed)).to.deep.equal(['main.js']);
return transformed['main.js'];
}

export function expectSource(src: string) {
return expect(transformSource(src));
var annotated = annotateSource(src);
// console.log('Annotated', annotated);
Copy link
Contributor

Choose a reason for hiding this comment

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

remove

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I'd like to keep. It's very spammy, but super useful to understand what's going on in one test. Maybe medium term we should have an expect with message for this.

var transformed = transformSource(annotated);
return expect(transformed);
}

export interface GoldenFileTest {
name: string;
tsPath: string;
jsPath: string;
}

export function goldenTests(): GoldenFileTest[] {
var tsExtRe = /\.ts$/;
var testFolder = path.join(__dirname, '..', '..', 'test_files');
var files = fs.readdirSync(testFolder).filter((fn) => !!fn.match(tsExtRe));
return files.map((fn) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for {} block if you only have a return statement, you can use arg => expression syntax as fn => {name: fn, ...}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You'd think - but not if what you're returning is an object literal:

This code gets parsed as a block with a goto label foo: and a sole property access bar:

var fn = (x) => { foo: bar };

return {
name: fn,
tsPath: path.join(testFolder, fn),
jsPath: path.join(testFolder, fn.replace(tsExtRe, '.js')),
};
});
}
1 change: 1 addition & 0 deletions test_files/arrow_fn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var fn3 = (/** number */ a) => 12;
1 change: 1 addition & 0 deletions test_files/arrow_fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
var fn3 = (a: number): number => 12;
4 changes: 4 additions & 0 deletions test_files/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @return { string} */ function fn1(/** number */ a) {
return "a";
}
function fn2(/** number */ a, /** number */ b) { }
4 changes: 4 additions & 0 deletions test_files/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
function fn1(a: number): string {
return "a";
}
function fn2(a: number, b: number) {}
2 changes: 2 additions & 0 deletions test_files/variables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
var /** string */ v1;
var /** string */ v2, /** number */ v3;
2 changes: 2 additions & 0 deletions test_files/variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
var v1: string;
var v2: string, v3: number;
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"files": [
"src/sickle.ts",
"test/sickle_test.ts",
"test/closure-compiler.d.ts",
"typings/tsd.d.ts"
]
}