diff --git a/gulpfile.js b/gulpfile.js index 4588a9fdc..cb349583a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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); }); @@ -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) { + 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']); diff --git a/package.json b/package.json index a8b7ece67..2e168c460 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/sickle.ts b/src/sickle.ts index e4265080e..5731b3391 100644 --- a/src/sickle.ts +++ b/src/sickle.ts @@ -14,10 +14,12 @@ export function formatDiagnostics(diags: ts.Diagnostic[]): string { .join('\n'); } -export type AnnotatedProgram = { +export type StringMap = { [fileName: string]: string }; +export type AnnotatedProgram = StringMap; + /** * A source processor that takes TypeScript code and annotates the output with Closure-style JSDoc * comments. @@ -27,7 +29,7 @@ class Annotator { constructor() {} - transform(args: string[]): AnnotatedProgram { + annotate(args: string[]): AnnotatedProgram { let tsArgs = ts.parseCommandLine(args); if (tsArgs.errors) { this.fail(formatDiagnostics(tsArgs.errors)); @@ -35,10 +37,10 @@ class Annotator { 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; @@ -136,13 +138,13 @@ function last(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)); } diff --git a/test/closure-compiler.d.ts b/test/closure-compiler.d.ts new file mode 100644 index 000000000..096d55cae --- /dev/null +++ b/test/closure-compiler.d.ts @@ -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; +} diff --git a/test/e2e_test.ts b/test/e2e_test.ts new file mode 100644 index 000000000..547198ccc --- /dev/null +++ b/test/e2e_test.ts @@ -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); + }); +}); diff --git a/test/sickle_test.ts b/test/sickle_test.ts index 65b87cc2a..87d50d758 100644 --- a/test/sickle_test.ts +++ b/test/sickle_test.ts @@ -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); }); }); }); diff --git a/test/test_support.ts b/test/test_support.ts index 2d6bdde49..c15ae98ca 100644 --- a/test/test_support.ts +++ b/test/test_support.ts @@ -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); + 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) => { + return { + name: fn, + tsPath: path.join(testFolder, fn), + jsPath: path.join(testFolder, fn.replace(tsExtRe, '.js')), + }; + }); } diff --git a/test_files/arrow_fn.js b/test_files/arrow_fn.js new file mode 100644 index 000000000..6a9e76003 --- /dev/null +++ b/test_files/arrow_fn.js @@ -0,0 +1 @@ +var fn3 = (/** number */ a) => 12; diff --git a/test_files/arrow_fn.ts b/test_files/arrow_fn.ts new file mode 100644 index 000000000..9ba372ea8 --- /dev/null +++ b/test_files/arrow_fn.ts @@ -0,0 +1 @@ +var fn3 = (a: number): number => 12; diff --git a/test_files/functions.js b/test_files/functions.js new file mode 100644 index 000000000..c11b2a462 --- /dev/null +++ b/test_files/functions.js @@ -0,0 +1,4 @@ +/** @return { string} */ function fn1(/** number */ a) { + return "a"; +} +function fn2(/** number */ a, /** number */ b) { } diff --git a/test_files/functions.ts b/test_files/functions.ts new file mode 100644 index 000000000..7484cba48 --- /dev/null +++ b/test_files/functions.ts @@ -0,0 +1,4 @@ +function fn1(a: number): string { + return "a"; +} +function fn2(a: number, b: number) {} diff --git a/test_files/variables.js b/test_files/variables.js new file mode 100644 index 000000000..a05a0b9e8 --- /dev/null +++ b/test_files/variables.js @@ -0,0 +1,2 @@ +var /** string */ v1; +var /** string */ v2, /** number */ v3; diff --git a/test_files/variables.ts b/test_files/variables.ts new file mode 100644 index 000000000..648ac0a33 --- /dev/null +++ b/test_files/variables.ts @@ -0,0 +1,2 @@ +var v1: string; +var v2: string, v3: number; diff --git a/tsconfig.json b/tsconfig.json index 43ab3564d..e722047b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "files": [ "src/sickle.ts", "test/sickle_test.ts", + "test/closure-compiler.d.ts", "typings/tsd.d.ts" ] }