-
Notifications
You must be signed in to change notification settings - Fork 109
feat: Closure compile, golden file base tests. #1
Changes from all commits
7a8b983
e104184
40ad718
63ed7b6
1783dd2
285811f
aa138f8
36b8651
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,10 +14,12 @@ export function formatDiagnostics(diags: ts.Diagnostic[]): string { | |
.join('\n'); | ||
} | ||
|
||
export type AnnotatedProgram = { | ||
export type StringMap = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe no need to export this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. | ||
|
@@ -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; | ||
|
@@ -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)); | ||
} |
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; | ||
} |
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); | ||
}); | ||
}); |
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); }); | ||
}); | ||
}); |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 var fn = (x) => { foo: bar }; |
||
return { | ||
name: fn, | ||
tsPath: path.join(testFolder, fn), | ||
jsPath: path.join(testFolder, fn.replace(tsExtRe, '.js')), | ||
}; | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
var fn3 = (/** number */ a) => 12; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
var fn3 = (a: number): number => 12; |
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) { } |
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) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
var /** string */ v1; | ||
var /** string */ v2, /** number */ v3; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
var v1: string; | ||
var v2: string, v3: number; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
"files": [ | ||
"src/sickle.ts", | ||
"test/sickle_test.ts", | ||
"test/closure-compiler.d.ts", | ||
"typings/tsd.d.ts" | ||
] | ||
} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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...