Improve doctests
This commit is contained in:
parent
9f0dd17419
commit
e1c62582cb
21 changed files with 1019 additions and 1929 deletions
138
README.md
138
README.md
|
@ -1,124 +1,30 @@
|
||||||
# doctest-ts: doctests for TypeScript
|
# doctest-ts-improved: doctests for TypeScript
|
||||||
|
|
||||||
Say you have a file src/hasFoo.ts with a function like hasFoo:
|
Easy doctests for typescript modules, including private methods and extra imports:
|
||||||
|
|
||||||
```typescript
|
```
|
||||||
function hasFoo(s: string): boolean {
|
export default class SomeClass {
|
||||||
return null != s.match(/foo/i)
|
/**
|
||||||
|
* Gets the field doubled
|
||||||
|
* @example xyz
|
||||||
|
*
|
||||||
|
* import OtherClass from "./OtherClass";
|
||||||
|
*
|
||||||
|
* // Should equal 42
|
||||||
|
* SomeClass.get() // => 42
|
||||||
|
*
|
||||||
|
* SomeClass.get() + 1 // => 43
|
||||||
|
*
|
||||||
|
* new OtherClass().doSomething(new SomeClass()) // => 5
|
||||||
|
*/
|
||||||
|
private static get() : number{
|
||||||
|
// a comment
|
||||||
|
// @ts-ignore
|
||||||
|
return 42
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now make documentation and unit tests for this function in one go:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/** Does this string contain foo, ignoring case?
|
|
||||||
|
|
||||||
hasFoo('___foo__') // => true
|
|
||||||
hasFoo(' fOO ') // => true
|
|
||||||
hasFoo('Foo.') // => true
|
|
||||||
hasFoo('bar') // => false
|
|
||||||
hasFoo('fo') // => false
|
|
||||||
hasFoo('oo') // => false
|
|
||||||
|
|
||||||
*/
|
|
||||||
function hasFoo(s: string): boolean {
|
|
||||||
return null != s.match(/foo/i)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Since the function is not exported we can only test this by either editing or copying the entire file and gluing on tests at the end.
|
|
||||||
This library goes for the second approach: making a copy of the file with the translated tests at the end. Run it like so:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ doctest-ts src/hasFoo.ts
|
|
||||||
Writing src/hasFoo.doctest.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
The contents of `src/hasFoo.doctest.ts` is the original file prepended to the doctests rewritten as unit tests.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/** Does this string contain foo, ignoring case?
|
|
||||||
|
|
||||||
hasFoo('___foo__') // => true
|
|
||||||
hasFoo(' fOO ') // => true
|
|
||||||
hasFoo('Foo.') // => true
|
|
||||||
hasFoo('bar') // => false
|
|
||||||
hasFoo('fo') // => false
|
|
||||||
hasFoo('oo') // => false
|
|
||||||
|
|
||||||
*/
|
|
||||||
function hasFoo(s: string): boolean {
|
|
||||||
return null != s.match(/foo/i)
|
|
||||||
}
|
|
||||||
|
|
||||||
import * as __test from "tape"
|
|
||||||
__test("hasFoo", t => {t.deepEqual(hasFoo("___foo__"), true, "true")
|
|
||||||
t.deepEqual(hasFoo(" fOO "), true, "true")
|
|
||||||
t.deepEqual(hasFoo("Foo."), true, "true")
|
|
||||||
t.deepEqual(hasFoo("bar"), false, "false")
|
|
||||||
t.deepEqual(hasFoo("fo"), false, "false")
|
|
||||||
t.deepEqual(hasFoo("oo"), false, "false")
|
|
||||||
;t.end()})
|
|
||||||
```
|
|
||||||
|
|
||||||
This can now be run with the tape runner or ts-node:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ts-node src/hasFoo.doctest.ts | tap-diff
|
|
||||||
hasFoo
|
|
||||||
✔ true
|
|
||||||
✔ true
|
|
||||||
✔ true
|
|
||||||
✔ false
|
|
||||||
✔ false
|
|
||||||
✔ false
|
|
||||||
Done in 0.37s.
|
|
||||||
|
|
||||||
passed: 6 failed: 0 of 6 tests (171ms)
|
|
||||||
|
|
||||||
All of 6 tests passed!
|
|
||||||
```
|
|
||||||
|
|
||||||
There are four different outputs available:
|
|
||||||
|
|
||||||
* tape
|
|
||||||
* AVA
|
|
||||||
* jest
|
|
||||||
* mocha (using chai)
|
|
||||||
|
|
||||||
Pull requests for other test runners are welcome.
|
|
||||||
|
|
||||||
## Watching file changes
|
|
||||||
|
|
||||||
We can tell `doctest-ts` to watch for file changes and report which files it has written.
|
|
||||||
It tries to be a good unix citizen and thus writes the files it has created on stdout (and some info on stderr).
|
|
||||||
This makes it possible to run test runners on each line on stdout like so:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ts-node src/main.ts --watch src/hasFo.ts |
|
|
||||||
while read file; do echo running tape on $file; ts-node $file | tap-diff; done
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's say we remove the ignore case `i` flag from the regex in `hasFoo`. We get this output (automatically):
|
|
||||||
```
|
|
||||||
Writing src/hasFoo.doctest.ts
|
|
||||||
running tape on src/hasFoo.doctest.ts
|
|
||||||
|
|
||||||
hasFoo
|
|
||||||
✔ true
|
|
||||||
✖ true at Test.t (src/hasFoo.doctest.ts:18:3)
|
|
||||||
[-false-][+true+]
|
|
||||||
✖ true at Test.t (src/hasFoo.doctest.ts:19:3)
|
|
||||||
[-false-][+true+]
|
|
||||||
✔ false
|
|
||||||
✔ false
|
|
||||||
✔ false
|
|
||||||
|
|
||||||
passed: 4 failed: 2 of 6 tests (264ms)
|
|
||||||
|
|
||||||
2 of 6 tests failed.
|
|
||||||
```
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
9
examples/OtherClass.ts
Normal file
9
examples/OtherClass.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import SomeClass from "./someClass";
|
||||||
|
|
||||||
|
export default class OtherClass {
|
||||||
|
|
||||||
|
public doSomething(c: SomeClass){
|
||||||
|
return c.xyz()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
export default class Example0 {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the field doubled
|
|
||||||
* @example xyz
|
|
||||||
*
|
|
||||||
* // Should equal 42
|
|
||||||
* Example0.get() // => 42
|
|
||||||
*
|
|
||||||
* Example0.get() + 1 // => 43
|
|
||||||
*/
|
|
||||||
private static get(){
|
|
||||||
// a comment
|
|
||||||
// @ts-ignore
|
|
||||||
return 42
|
|
||||||
}
|
|
||||||
|
|
||||||
public testMore(){
|
|
||||||
return ({} as any) ?.xyz?.abc ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
27
examples/someClass.ts
Normal file
27
examples/someClass.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
export default class SomeClass {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the field doubled
|
||||||
|
* @example xyz
|
||||||
|
*
|
||||||
|
* import OtherClass from "./OtherClass";
|
||||||
|
*
|
||||||
|
* // Should equal 42
|
||||||
|
* SomeClass.get() // => 42
|
||||||
|
*
|
||||||
|
* SomeClass.get() + 1 // => 43
|
||||||
|
*
|
||||||
|
* new OtherClass().doSomething(new SomeClass()) // => 5
|
||||||
|
*/
|
||||||
|
private static get() : number{
|
||||||
|
// a comment
|
||||||
|
// @ts-ignore
|
||||||
|
return 42
|
||||||
|
}
|
||||||
|
|
||||||
|
public xyz(){
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
package.json
17
package.json
|
@ -1,17 +1,16 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://json.schemastore.org/package",
|
"$schema": "http://json.schemastore.org/package",
|
||||||
"name": "doctest-ts",
|
"name": "doctest-ts-improved",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"description": "doctest support for typescript",
|
"description": "doctest support for typescript with Mocha",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"doctest-ts": "dist/src/main.js"
|
"doctest-ts-improved": "dist/src/main.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && chmod 755 dist/src/main.js",
|
"build": "tsc && chmod 755 dist/src/main.js",
|
||||||
"test": "ts-node src/main.ts --tape src/*ts test/*ts && ts-node node_modules/.bin/tape test/*.ts src/*doctest*.ts | tap-diff",
|
|
||||||
"doctest:watch": "ts-node src/main.ts --tape --watch {src,test}/*.ts | while read file; do echo tape $file; ts-node $file | tap-diff; done",
|
"doctest:watch": "ts-node src/main.ts --tape --watch {src,test}/*.ts | while read file; do echo tape $file; ts-node $file | tap-diff; done",
|
||||||
"debug": "ts-node src/main.ts --mocha examples/example0.ts",
|
"test": "ts-node src/main.ts examples/ && ts-node src/main.ts src/ && mocha --require ts-node/register examples/*.ts src/*.ts",
|
||||||
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*"
|
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -30,14 +29,16 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/danr/doctest-ts#readme",
|
"homepage": "https://github.com/danr/doctest-ts#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^2.0.1",
|
"@types/chai": "^4.3.0",
|
||||||
|
"chai": "^4.3.6",
|
||||||
"global": "^4.3.2",
|
"global": "^4.3.2",
|
||||||
"minimist": "^1.2.0",
|
"mocha": "^9.2.2",
|
||||||
"typescript": "^4.6.2"
|
"typescript": "^4.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chokidar": "^1.7.5",
|
"@types/chokidar": "^1.7.5",
|
||||||
"@types/minimist": "^1.2.0",
|
"@types/minimist": "^1.2.0",
|
||||||
|
"@types/mocha": "^9.1.0",
|
||||||
"@types/node": "^9.4.6",
|
"@types/node": "^9.4.6",
|
||||||
"@types/tape": "^4.2.31",
|
"@types/tape": "^4.2.31",
|
||||||
"faucet": "^0.0.1",
|
"faucet": "^0.0.1",
|
||||||
|
|
97
src/ExtractComments.ts
Normal file
97
src/ExtractComments.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import * as ts from "typescript";
|
||||||
|
import {JSDocComment} from "typescript";
|
||||||
|
import {SyntaxKind} from "typescript/lib/tsserverlibrary";
|
||||||
|
|
||||||
|
export interface Context {
|
||||||
|
filepath: string,
|
||||||
|
linenumber?: number,
|
||||||
|
classname?: string,
|
||||||
|
functionname?: string,
|
||||||
|
testname?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for extracting the testfiles from the .ts files
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class ExtractComments {
|
||||||
|
|
||||||
|
private readonly results: { comment: string, context: Context }[] = []
|
||||||
|
private readonly code: string;
|
||||||
|
|
||||||
|
constructor(filepath: string, code: string) {
|
||||||
|
this.code = code;
|
||||||
|
const ast = ts.createSourceFile('_.ts', code, ts.ScriptTarget.Latest)
|
||||||
|
this.traverse(ast, {filepath: filepath})
|
||||||
|
}
|
||||||
|
|
||||||
|
public getComments(): { comment: string, context: Context }[] {
|
||||||
|
return this.results
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLineNumber(pos: number) {
|
||||||
|
let line = 0;
|
||||||
|
for (let i = 0; i < pos; i++) {
|
||||||
|
if (this.code[i] === "\n") {
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerComment(context: Context, comment: string | ts.NodeArray<JSDocComment> | undefined, position: number) {
|
||||||
|
if (comment === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
context = {...context, linenumber: this.getLineNumber(position)}
|
||||||
|
if (typeof comment === "string") {
|
||||||
|
this.results.push({comment: comment || '', context});
|
||||||
|
} else {
|
||||||
|
comment.forEach(jsDocComment => {
|
||||||
|
this.results.push({comment: jsDocComment.text || '', context});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private traverse(node: ts.Node, context: Context) {
|
||||||
|
if (SyntaxKind.ClassDeclaration === node.kind) {
|
||||||
|
context = {...context, classname: (node as any)["name"].escapedText};
|
||||||
|
}
|
||||||
|
const jsdocs = (node as any).jsDoc || []
|
||||||
|
if (jsdocs.length > 0) {
|
||||||
|
let declName = undefined
|
||||||
|
try {
|
||||||
|
declName = (node as any).name.escapedText
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
const decls = (node as any).declarationList.declarations
|
||||||
|
if (decls.length == 1) {
|
||||||
|
declName = decls[0].name.escapedText || null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
declName = ts.isConstructorDeclaration(node) ? 'constructor' : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {...context, functionname: declName}
|
||||||
|
|
||||||
|
jsdocs.forEach((doc: ts.JSDoc) => {
|
||||||
|
this.registerComment(context, doc.comment, doc.pos)
|
||||||
|
|
||||||
|
// A part of the comment might be in the tags; we simply add those too and figure out later if they contain doctests
|
||||||
|
const tags = doc.tags;
|
||||||
|
if (tags !== undefined) {
|
||||||
|
tags.forEach(tag => {
|
||||||
|
this.registerComment(context, tag.comment, tag.pos)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ts.forEachChild(node, n => this.traverse(n, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
131
src/TestCreator.ts
Normal file
131
src/TestCreator.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import UnitTest from "./UnitTest";
|
||||||
|
import * as ts from "typescript";
|
||||||
|
import ExtractComments from "./ExtractComments";
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for creating a '.doctest.ts'-file
|
||||||
|
*/
|
||||||
|
export default class TestCreator {
|
||||||
|
private _originalFilepath: string;
|
||||||
|
|
||||||
|
constructor(originalFilepath: string) {
|
||||||
|
this._originalFilepath = originalFilepath;
|
||||||
|
if (originalFilepath.includes('doctest')) {
|
||||||
|
throw "Not creating a doctest for a file which already is a doctest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static exposePrivates(s: string): string {
|
||||||
|
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest) as ts.SourceFile
|
||||||
|
|
||||||
|
const transformer = <T extends ts.Node>(context: ts.TransformationContext) =>
|
||||||
|
(rootNode: T) => {
|
||||||
|
function visit(node: ts.Node): ts.Node {
|
||||||
|
if (node.kind === ts.SyntaxKind.PrivateKeyword) {
|
||||||
|
return ts.createModifier(ts.SyntaxKind.PublicKeyword)
|
||||||
|
}
|
||||||
|
return ts.visitEachChild(node, visit, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.visitNode(rootNode, visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformed = ts.transform(ast, [transformer]).transformed[0]
|
||||||
|
|
||||||
|
const pwoc = ts.createPrinter({removeComments: false})
|
||||||
|
return pwoc.printNode(ts.EmitHint.Unspecified, transformed, ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static testCode(tests: UnitTest[]): string[] {
|
||||||
|
const code: string[] = []
|
||||||
|
const exportedTests = new Set<UnitTest>()
|
||||||
|
|
||||||
|
function show(s: string) {
|
||||||
|
return JSON.stringify(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emit(test: UnitTest, indent: string = "") {
|
||||||
|
if (exportedTests.has(test)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const testCode = "\n" + test.generateCode()
|
||||||
|
code.push(testCode.replace(/\n/g, "\n" + indent))
|
||||||
|
exportedTests.add(test)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitAllForFunction(functionname: string | undefined, indent: string) {
|
||||||
|
tests.filter(t => t.context.functionname === functionname).forEach(c => emit(c, " " + indent))
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitAllForClass(classname: string | undefined, indent: string) {
|
||||||
|
const forClass: UnitTest[] = tests.filter(t => t.context.classname === classname)
|
||||||
|
for (const test of forClass) {
|
||||||
|
if (exportedTests.has(test)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (test.context.functionname !== undefined) {
|
||||||
|
code.push(indent+"describe(" + show(test.context.functionname) + ", () => {")
|
||||||
|
emitAllForFunction(test.context.functionname, " " + indent)
|
||||||
|
code.push(indent+"})")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
emitAllForFunction(undefined, indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
if (exportedTests.has(test)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (test.context.classname !== undefined) {
|
||||||
|
code.push("describe(" + show(test.context.classname) + ", () => {")
|
||||||
|
emitAllForClass(test.context.classname, " ")
|
||||||
|
code.push("})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitAllForClass(undefined, "")
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new file with the doctests.
|
||||||
|
*
|
||||||
|
* Returns the number of found tests
|
||||||
|
*/
|
||||||
|
public createTest(): number {
|
||||||
|
const file = this._originalFilepath
|
||||||
|
const {base, ext, ...u} = path.parse(file)
|
||||||
|
const buffer = fs.readFileSync(file, {encoding: 'utf8'})
|
||||||
|
const comments = new ExtractComments(file, buffer).getComments()
|
||||||
|
const tests: UnitTest[] = UnitTest.FromComments(comments)
|
||||||
|
|
||||||
|
const outfile = path.format({...u, ext: '.doctest' + ext})
|
||||||
|
if (tests.length == 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = []
|
||||||
|
const imports = new Set<string>()
|
||||||
|
for (const test of tests) {
|
||||||
|
test.getImports().forEach(i => imports.add(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add imports needed by the tests
|
||||||
|
code.push(...Array.from(imports))
|
||||||
|
// Adds the original code where the private keywords are removed
|
||||||
|
code.push(TestCreator.exposePrivates(buffer))
|
||||||
|
|
||||||
|
// At last, we add all the doctests
|
||||||
|
code.push(...TestCreator.testCode(tests))
|
||||||
|
|
||||||
|
fs.writeFileSync(outfile, code.join("\n"))
|
||||||
|
return tests.length
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
17
src/TestSuite.ts
Normal file
17
src/TestSuite.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export default interface TestDescription {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the module under test
|
||||||
|
*/
|
||||||
|
moduleName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the function under testing
|
||||||
|
*/
|
||||||
|
functionName?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the singular test
|
||||||
|
*/
|
||||||
|
testName?: string
|
||||||
|
}
|
150
src/UnitTest.ts
Normal file
150
src/UnitTest.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import {Context} from "./ExtractComments";
|
||||||
|
import * as ts from "typescript";
|
||||||
|
|
||||||
|
type Script = (Statement | Equality)[]
|
||||||
|
interface Equality {
|
||||||
|
tag: '=='
|
||||||
|
lhs: string
|
||||||
|
rhs: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Statement {
|
||||||
|
tag: 'Statement'
|
||||||
|
stmt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScriptExtraction {
|
||||||
|
/**
|
||||||
|
* ScriptExtraction.is_doctest('// => true') // => true
|
||||||
|
* ScriptExtraction.is_doctest('// true') // => false
|
||||||
|
*/
|
||||||
|
public static is_doctest(s: string): boolean {
|
||||||
|
return s.match(/\/\/[ \t]*=>/) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the expected value
|
||||||
|
*
|
||||||
|
* const m = ScriptExtraction.doctest_rhs('// => true') || []
|
||||||
|
* m[1] // => ' true'
|
||||||
|
*/
|
||||||
|
public static doctest_rhs(s: string) {
|
||||||
|
return s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static extractImports(docstring: string){
|
||||||
|
return docstring.split("\n").filter(s => s.startsWith("import "));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static extractScripts(docstring: string): { script: Script, name?: string, line: number }[] {
|
||||||
|
const out = [] as { script: Script, name?: string, line: number }[]
|
||||||
|
let line = 0;
|
||||||
|
for (const s of docstring.split(/\n\n+/m)) {
|
||||||
|
const p: number = line;
|
||||||
|
line += s.split(/\r\n|\r|\n/).length
|
||||||
|
|
||||||
|
if (!ScriptExtraction.is_doctest(s)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = ScriptExtraction.extractScript(s)
|
||||||
|
let name = undefined
|
||||||
|
const match = s.match(/^[ \t]*\/\/([^\n]*)/)
|
||||||
|
if (match !== null) {
|
||||||
|
name = match[1].trim()
|
||||||
|
}
|
||||||
|
out.push({script, name, line: p})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScriptExtraction.extractScript('s') // => [{tag: 'Statement', stmt: 's;'}]
|
||||||
|
* ScriptExtraction.extractScript('e // => 1') // => [{tag: '==', lhs: 'e', rhs: '1'}]
|
||||||
|
* ScriptExtraction.extractScript('s; e // => 1') // => [{tag: 'Statement', stmt: 's;'}, {tag: '==', lhs: 'e', rhs: '1'}]
|
||||||
|
*/
|
||||||
|
private static extractScript(s: string): Script {
|
||||||
|
const pwoc = ts.createPrinter({removeComments: true})
|
||||||
|
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
||||||
|
return ast.statements.map((stmt, i): Statement | Equality => {
|
||||||
|
if (ts.isExpressionStatement(stmt)) {
|
||||||
|
const next = ast.statements[i + 1] // zip with next
|
||||||
|
const [a, z] = next ? [next.pos, next.end] : [stmt.end, ast.end]
|
||||||
|
const after = ast.text.slice(a, z)
|
||||||
|
const m = ScriptExtraction.doctest_rhs(after)
|
||||||
|
if (m && m[1]) {
|
||||||
|
const lhs = pwoc.printNode(ts.EmitHint.Expression, stmt.expression, ast)
|
||||||
|
const rhs = m[1].trim()
|
||||||
|
return {tag: '==', lhs, rhs}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return {tag: 'Statement', stmt: pwoc.printNode(ts.EmitHint.Unspecified, stmt, ast)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single unit test somewhere in a file.
|
||||||
|
*/
|
||||||
|
export default class UnitTest {
|
||||||
|
|
||||||
|
|
||||||
|
public body: Script;
|
||||||
|
public context: Context;
|
||||||
|
private _extraImports: string[];
|
||||||
|
|
||||||
|
private constructor(body: Script, context: Context, extraImports: string[]) {
|
||||||
|
this.body = body;
|
||||||
|
this.context = context;
|
||||||
|
this._extraImports = extraImports;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FromComment(comment: string, context: Context): UnitTest[] {
|
||||||
|
const imports = ScriptExtraction.extractImports(comment)
|
||||||
|
return ScriptExtraction.extractScripts(comment).map(({script, line, name}, i) =>
|
||||||
|
new UnitTest(script, {
|
||||||
|
...context,
|
||||||
|
linenumber: (context.linenumber ?? 0) + line,
|
||||||
|
testname: name ?? 'doctest ' + i
|
||||||
|
},imports))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FromComments(comms: { comment: string, context: Context }[]): UnitTest[] {
|
||||||
|
const result: UnitTest[] = []
|
||||||
|
for (const comm of comms) {
|
||||||
|
result.push(...UnitTest.FromComment(comm.comment, comm.context))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public getImports(): string[] {
|
||||||
|
return [...this._extraImports, 'import "mocha"', 'import {expect as __expect} from "chai"']
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the mocha test code for this unit test.
|
||||||
|
* Will only construct the 'it('should ....') { __expect(x).deep.eq(y) } part
|
||||||
|
*/
|
||||||
|
public generateCode() {
|
||||||
|
const script = this.body
|
||||||
|
.map(s => {
|
||||||
|
if (s.tag == 'Statement') {
|
||||||
|
return s.stmt
|
||||||
|
} else {
|
||||||
|
return `__expect(${s.lhs}, "failed at ${this.context.functionname} (${this.context.filepath}:${this.context.linenumber}:1)").to.deep.equal(${s.rhs})`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(x => '\n ' + x)
|
||||||
|
.join('')
|
||||||
|
return `it(${this.getName()}, () => {${script}\n})`
|
||||||
|
}
|
||||||
|
|
||||||
|
private getName() {
|
||||||
|
return JSON.stringify(this.context.testname ?? this.context.functionname)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
276
src/internal.ts
276
src/internal.ts
|
@ -1,276 +0,0 @@
|
||||||
import * as ts from 'typescript'
|
|
||||||
import {JSDocComment} from 'typescript'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as path from 'path'
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
|
||||||
// Types
|
|
||||||
|
|
||||||
export interface Equality {
|
|
||||||
tag: '=='
|
|
||||||
lhs: string
|
|
||||||
rhs: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Statement {
|
|
||||||
tag: 'Statement'
|
|
||||||
stmt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Script = (Statement | Equality)[]
|
|
||||||
|
|
||||||
export type Context = string | null
|
|
||||||
|
|
||||||
export interface Comment {
|
|
||||||
comment: string
|
|
||||||
context: Context
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
|
||||||
// Extracting docstrings from program
|
|
||||||
|
|
||||||
export function Comments(s: string): Comment[] {
|
|
||||||
const out: Comment[] = []
|
|
||||||
function registerComment(context: string | null,comment: string | ts.NodeArray<JSDocComment> | undefined){
|
|
||||||
if(comment === undefined){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if(typeof comment === "string"){
|
|
||||||
out.push({comment: comment || '', context});
|
|
||||||
}else{
|
|
||||||
comment.forEach(jsDocComment => {
|
|
||||||
out.push({comment: jsDocComment.text || '', context});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function traverse(node: ts.Node) {
|
|
||||||
const jsdocs = (node as any).jsDoc || []
|
|
||||||
if (jsdocs.length > 0) {
|
|
||||||
let context: string | null = null
|
|
||||||
try {
|
|
||||||
context = (node as any).name.escapedText || null
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
const decls = (node as any).declarationList.declarations
|
|
||||||
if (decls.length == 1) {
|
|
||||||
context = decls[0].name.escapedText || null
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
context = ts.isConstructorDeclaration(node) ? 'constructor' : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsdocs.forEach((doc: ts.JSDoc) => {
|
|
||||||
registerComment(context, doc.comment)
|
|
||||||
|
|
||||||
// A part of the comment might be in the tags; we simply add those too and figure out later if they contain doctests
|
|
||||||
const tags = doc.tags;
|
|
||||||
if(tags !== undefined){
|
|
||||||
tags.forEach(tag => {
|
|
||||||
registerComment(context, tag.comment)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
ts.forEachChild(node, traverse)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
|
||||||
traverse(ast)
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
|
||||||
// Extracting test scripts from docstrings
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
is_doctest('// => true') // => true
|
|
||||||
is_doctest('// true') // => false
|
|
||||||
|
|
||||||
*/
|
|
||||||
const is_doctest = (s: string) => s.match(/\/\/[ \t]*=>/) != null
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
const m = doctest_rhs('// => true') || []
|
|
||||||
m[1] // => ' true'
|
|
||||||
|
|
||||||
*/
|
|
||||||
const doctest_rhs = (s: string) => s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m)
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
extractScript('s') // => [{tag: 'Statement', stmt: 's;'}]
|
|
||||||
|
|
||||||
extractScript('e // => 1') // => [{tag: '==', lhs: 'e', rhs: '1'}]
|
|
||||||
|
|
||||||
extractScript('s; e // => 1') // => [{tag: 'Statement', stmt: 's;'}, {tag: '==', lhs: 'e', rhs: '1'}]
|
|
||||||
|
|
||||||
*/
|
|
||||||
export function extractScript(s: string): Script {
|
|
||||||
const pwoc = ts.createPrinter({removeComments: true})
|
|
||||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
|
||||||
return ast.statements.map((stmt, i): Statement | Equality => {
|
|
||||||
if (ts.isExpressionStatement(stmt)) {
|
|
||||||
const next = ast.statements[i + 1] // zip with next
|
|
||||||
const [a, z] = next ? [next.pos, next.end] : [stmt.end, ast.end]
|
|
||||||
const after = ast.text.slice(a, z)
|
|
||||||
const m = doctest_rhs(after)
|
|
||||||
if (m && m[1]) {
|
|
||||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, stmt.expression, ast)
|
|
||||||
const rhs = m[1].trim()
|
|
||||||
return {tag: '==', lhs, rhs}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {tag: 'Statement', stmt: pwoc.printNode(ts.EmitHint.Unspecified, stmt, ast)}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractScripts(docstring: string): {script: Script, name?: string}[] {
|
|
||||||
const out = [] as {script: Script, name?: string}[]
|
|
||||||
docstring.split(/\n\n+/m).forEach(s => {
|
|
||||||
if (is_doctest(s)) {
|
|
||||||
const script = extractScript(s)
|
|
||||||
let name = undefined
|
|
||||||
const match = s.match(/^[ \t]*\/\/([^\n]*)/)
|
|
||||||
if(match !== null)
|
|
||||||
{
|
|
||||||
name = match[1].trim()
|
|
||||||
}
|
|
||||||
out.push({script, name})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
|
||||||
// Showing test scripts
|
|
||||||
export interface ShowScript {
|
|
||||||
showImports: string
|
|
||||||
showScript(script: Script, c: Context, name?: string): string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** show("hello") // => '"hello"' */
|
|
||||||
export function show(s: any) {
|
|
||||||
return JSON.stringify(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showContext(c: Context) {
|
|
||||||
return show(c || 'doctest')
|
|
||||||
}
|
|
||||||
|
|
||||||
function tapeOrAVA(script: Script, c: Context, name: string | undefined, before_end = (t: string) => '') {
|
|
||||||
const t = `t`
|
|
||||||
const body = script
|
|
||||||
.map(s => {
|
|
||||||
if (s.tag == 'Statement') {
|
|
||||||
return s.stmt
|
|
||||||
} else {
|
|
||||||
return `${t}.deepEqual(${s.lhs}, ${s.rhs}, ${show(s.rhs)})`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(x => '\n ' + x)
|
|
||||||
.join('')
|
|
||||||
return `
|
|
||||||
__test(${showContext(c)}, ${t} => {
|
|
||||||
${body}
|
|
||||||
${before_end(t)}
|
|
||||||
})`
|
|
||||||
}
|
|
||||||
|
|
||||||
const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c, name) => {
|
|
||||||
const body = script
|
|
||||||
.map(s => {
|
|
||||||
if (s.tag == 'Statement') {
|
|
||||||
return s.stmt
|
|
||||||
} else {
|
|
||||||
return `__expect(${s.lhs}).${deepEqual}(${s.rhs})`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(x => '\n ' + x)
|
|
||||||
.join('')
|
|
||||||
|
|
||||||
return `
|
|
||||||
describe(${showContext(c)}, () => {
|
|
||||||
it(${show(name) || showContext(c)}, () => {${body}})
|
|
||||||
})
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const showScriptInstances: Record<string, ShowScript> = {
|
|
||||||
ava: {
|
|
||||||
showImports: 'import {test as __test} from "ava"',
|
|
||||||
showScript: tapeOrAVA,
|
|
||||||
},
|
|
||||||
|
|
||||||
tape: {
|
|
||||||
showImports: 'import * as __test from "tape"',
|
|
||||||
showScript: (s, c, name) => tapeOrAVA(s, c, name, t => `\n;${t}.end()`),
|
|
||||||
},
|
|
||||||
|
|
||||||
mocha: {
|
|
||||||
showImports: 'import "mocha"\nimport {expect as __expect} from "chai"',
|
|
||||||
showScript: mochaOrJest(`to.deep.equal`),
|
|
||||||
},
|
|
||||||
|
|
||||||
jest: {
|
|
||||||
showImports: 'import "jest"\nconst __expect: jest.Expect = expect',
|
|
||||||
showScript: mochaOrJest(`toEqual`),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function exposePrivates(s: string): string {
|
|
||||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest) as ts.SourceFile
|
|
||||||
|
|
||||||
const transformer = <T extends ts.Node>(context: ts.TransformationContext) =>
|
|
||||||
(rootNode: T) => {
|
|
||||||
function visit(node: ts.Node): ts.Node {
|
|
||||||
if (node.kind === ts.SyntaxKind.PrivateKeyword) {
|
|
||||||
return ts.createModifier(ts.SyntaxKind.PublicKeyword)
|
|
||||||
}
|
|
||||||
return ts.visitEachChild(node, visit, context);
|
|
||||||
}
|
|
||||||
return ts.visitNode(rootNode, visit);
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformed = ts.transform(ast, [transformer]).transformed[0]
|
|
||||||
|
|
||||||
const pwoc = ts.createPrinter({ removeComments: false})
|
|
||||||
return pwoc.printNode(ts.EmitHint.Unspecified, transformed, ast)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
|
|
||||||
const {base, ext, ...u} = path.parse(file)
|
|
||||||
if (base.includes('doctest')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const buffer = fs.readFileSync(file, {encoding: 'utf8'})
|
|
||||||
const withoutPrivates = exposePrivates(buffer)
|
|
||||||
const tests = Doctests(d, buffer)
|
|
||||||
const outfile = path.format({...u, ext: '.doctest' + ext})
|
|
||||||
if (tests.length == 0) {
|
|
||||||
console.error('No doctests found in', file)
|
|
||||||
} else {
|
|
||||||
console.error('Writing', outfile)
|
|
||||||
if (mode == 'watch') {
|
|
||||||
console.log(outfile)
|
|
||||||
}
|
|
||||||
fs.writeFileSync(outfile, withoutPrivates + '\n' + d.showImports + '\n' + tests.join('\n'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Doctests(d: ShowScript, buffer: string): string[] {
|
|
||||||
const out: string[] = []
|
|
||||||
for (const c of Comments(buffer)) {
|
|
||||||
for (const script of extractScripts(c.comment)) {
|
|
||||||
out.push(d.showScript(script.script, c.context, script.name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
93
src/main.ts
93
src/main.ts
|
@ -1,49 +1,52 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import * as chokidar from 'chokidar'
|
import {lstatSync, readdirSync} from "fs";
|
||||||
import * as minimist from 'minimist'
|
import TestCreator from "./TestCreator";
|
||||||
import {instrument, showScriptInstances} from './internal'
|
|
||||||
function main() {
|
|
||||||
const outputs = Object.keys(showScriptInstances)
|
function readDirRecSync(path: string, maxDepth = 999): string[] {
|
||||||
const flags = outputs.map(f => '--' + f)
|
const result = []
|
||||||
const boolean = ['watch'].concat(outputs)
|
if (maxDepth <= 0) {
|
||||||
const opts = minimist(process.argv.slice(2), {boolean})
|
return []
|
||||||
let output: string | null = null
|
|
||||||
let error: string | null = null
|
|
||||||
outputs.forEach(k => {
|
|
||||||
if (opts[k]) {
|
|
||||||
if (output != null) {
|
|
||||||
error = `Cannot output both ${output} and ${k}`
|
|
||||||
}
|
|
||||||
output = k
|
|
||||||
}
|
}
|
||||||
})
|
for (const entry of readdirSync(path)) {
|
||||||
if (output == null) {
|
const fullEntry = path + "/" + entry
|
||||||
error = `Choose an output from ${flags.join(' ')}`
|
const stats = lstatSync(fullEntry)
|
||||||
}
|
if (stats.isDirectory()) {
|
||||||
const files = opts._
|
// Subdirectory
|
||||||
if (files.length == 0 || output == null) {
|
// @ts-ignore
|
||||||
console.error(
|
result.push(...ScriptUtils.readDirRecSync(fullEntry, maxDepth - 1))
|
||||||
`
|
} else {
|
||||||
Error: ${error || `No files specified!`}
|
result.push(fullEntry)
|
||||||
|
}
|
||||||
Usage:
|
}
|
||||||
|
return result;
|
||||||
${flags.join('|')} [-w|--watch] files globs...
|
|
||||||
|
|
||||||
Your options were:`,
|
|
||||||
opts,
|
|
||||||
`
|
|
||||||
From:`,
|
|
||||||
process.argv
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const d = showScriptInstances[output]
|
|
||||||
files.forEach(file => instrument(d, file))
|
|
||||||
if (opts.w == true || opts.watch == true) {
|
|
||||||
const watcher = chokidar.watch(files, {ignored: '*.doctest.*'})
|
|
||||||
watcher.on('change', file => global.setTimeout(() => instrument(d, file, 'watch'), 25))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
function main(){
|
||||||
|
|
||||||
|
const args = process.argv
|
||||||
|
console.log(args.join(","))
|
||||||
|
const directory = args[2].replace(/\/$/, "")
|
||||||
|
if(directory === "--require"){
|
||||||
|
console.error("Probably running the testsuite, detects '--require' as second argument. Quitting now")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(directory === undefined){
|
||||||
|
console.log("Usage: doctest-ts-improved <directory under test>. This will automatically scan recursively for '.ts'-files, excluding 'node_modules' '*.doctest.ts'-files")
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readDirRecSync(directory).filter(p => !p.startsWith("./node_modules") && !p.endsWith(".doctest.ts"))
|
||||||
|
const noTests : string[] = []
|
||||||
|
for (const file of files) {
|
||||||
|
const generated = new TestCreator(file).createTest()
|
||||||
|
if(generated === 0){
|
||||||
|
noTests.push(file)
|
||||||
|
}else{
|
||||||
|
console.log("Generated tests for "+file+" ("+generated+" tests found)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(noTests.length > 0){
|
||||||
|
console.log("No tests found in: "+noTests.join(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main()
|
|
@ -1,13 +0,0 @@
|
||||||
/** Does this string contain foo, ignoring case?
|
|
||||||
|
|
||||||
hasFoo('___foo__') // => true
|
|
||||||
hasFoo(' fOO ') // => true
|
|
||||||
hasFoo('Foo.') // => true
|
|
||||||
hasFoo('bar') // => false
|
|
||||||
hasFoo('fo') // => false
|
|
||||||
hasFoo('oo') // => false
|
|
||||||
|
|
||||||
*/
|
|
||||||
function hasFoo(s: string): boolean {
|
|
||||||
return null != s.match(/foo/i)
|
|
||||||
}
|
|
152
test/test.ts
152
test/test.ts
|
@ -1,152 +0,0 @@
|
||||||
import * as internal from '../src/internal'
|
|
||||||
import * as test from 'tape'
|
|
||||||
|
|
||||||
test('tests', t => {
|
|
||||||
t.plan(1)
|
|
||||||
t.deepEqual(
|
|
||||||
internal.extractScripts(`*
|
|
||||||
|
|
||||||
foo // => 1
|
|
||||||
|
|
||||||
`),
|
|
||||||
[{ script: [{tag: '==', lhs: `foo`, rhs: `1`}], name: undefined }]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('tests', t => {
|
|
||||||
t.plan(1)
|
|
||||||
t.deepEqual(
|
|
||||||
internal.extractScripts(`*
|
|
||||||
|
|
||||||
a
|
|
||||||
b // => 1 + 2 + 3
|
|
||||||
c // => 1
|
|
||||||
d
|
|
||||||
|
|
||||||
*/`).map(s => s.script),
|
|
||||||
[
|
|
||||||
[
|
|
||||||
{tag: 'Statement', stmt: 'a;'},
|
|
||||||
{tag: '==', lhs: 'b', rhs: '1 + 2 + 3'},
|
|
||||||
{tag: '==', lhs: 'c', rhs: '1'},
|
|
||||||
{tag: 'Statement', stmt: 'd;'},
|
|
||||||
],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const c = (comment: string, context: string | null) => ({comment, context})
|
|
||||||
|
|
||||||
test('modules and namespace', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** m */
|
|
||||||
namespace m {}
|
|
||||||
|
|
||||||
/** ns */
|
|
||||||
namespace ns {}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('m', 'm'), c('ns', 'ns')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('const', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** u */
|
|
||||||
const u = 1
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('u', 'u')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('const object', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** k */
|
|
||||||
const k = {
|
|
||||||
/** a */
|
|
||||||
a: 1,
|
|
||||||
/** b */
|
|
||||||
b(x: string) { return x+x }
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('k', 'k'), c('a', 'a'), c('b', 'b')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('object deconstruction', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** hello */
|
|
||||||
const {u, v} = {u: 1, v: 2}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('hello', null)])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('function', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** v */
|
|
||||||
function v(s: string): number {
|
|
||||||
return s.length + 1
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('v', 'v')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('class', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** C */
|
|
||||||
class C<A> {
|
|
||||||
/** constructor */
|
|
||||||
constructor() {}
|
|
||||||
/** m */
|
|
||||||
m(s: Array<number>): Array<string> {
|
|
||||||
}
|
|
||||||
/** p */
|
|
||||||
p: Array<number>
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('C', 'C'), c('constructor', 'constructor'), c('m', 'm'), c('p', 'p')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('interface', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** I */
|
|
||||||
interface I<A> {
|
|
||||||
/** i */
|
|
||||||
i: A,
|
|
||||||
/** j */
|
|
||||||
j(a: A): string
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('I', 'I'), c('i', 'i'), c('j', 'j')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('type', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
/** T */
|
|
||||||
type T = number
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('T', 'T')])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('anywhere', t => {
|
|
||||||
t.plan(1)
|
|
||||||
const cs = internal.Comments(`
|
|
||||||
const $ = () => {
|
|
||||||
/** test1 */
|
|
||||||
const w = 1
|
|
||||||
|
|
||||||
/** test2 */
|
|
||||||
function f(x) {
|
|
||||||
return x * x
|
|
||||||
}
|
|
||||||
|
|
||||||
/** test3 */
|
|
||||||
return f(f(w))
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
t.deepEqual(cs, [c('test1', 'w'), c('test2', 'f'), c('test3', null)])
|
|
||||||
})
|
|
|
@ -1,21 +1,23 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"lib": ["es6"],
|
"lib": [
|
||||||
"noEmitOnError": true,
|
"es6"
|
||||||
"allowUnreachableCode": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src",
|
|
||||||
"test"
|
|
||||||
],
|
],
|
||||||
"parcelTsPluginOptions": {
|
"noEmitOnError": true,
|
||||||
"transpileOnly": false
|
"allowUnreachableCode": true
|
||||||
}
|
},
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"parcelTsPluginOptions": {
|
||||||
|
"transpileOnly": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
class X {
|
|
||||||
J: {
|
|
||||||
u: number
|
|
||||||
} = {
|
|
||||||
u: 1
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const j = 1
|
|
||||||
export module A {
|
|
||||||
export const c = 1
|
|
||||||
export const s = 1
|
|
||||||
export interface J {
|
|
||||||
u: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export namespace N {
|
|
||||||
export const d = 1
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
|
|
||||||
/** I */
|
|
||||||
export interface I {
|
|
||||||
/** k */
|
|
||||||
k: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** H */
|
|
||||||
interface H {
|
|
||||||
/** k */
|
|
||||||
k: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** C */
|
|
||||||
export class C {
|
|
||||||
/** f */
|
|
||||||
f = 1
|
|
||||||
/** g */
|
|
||||||
g: number
|
|
||||||
/** new */
|
|
||||||
constructor(x: number) { this.g = x }
|
|
||||||
/** s */
|
|
||||||
static s(y: number): I { return {k: y} }
|
|
||||||
/** m */
|
|
||||||
m(z: number): I { return {k: z} }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** T */
|
|
||||||
type T = C
|
|
||||||
|
|
||||||
/** c */
|
|
||||||
const c = new C(1)
|
|
||||||
|
|
||||||
/** M */
|
|
||||||
export module M {
|
|
||||||
/** MI */
|
|
||||||
export interface MI {
|
|
||||||
/** M k */
|
|
||||||
k: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** MC */
|
|
||||||
export class MC {
|
|
||||||
/** M f */
|
|
||||||
f = 1
|
|
||||||
/** M g */
|
|
||||||
g: number
|
|
||||||
/** M new */
|
|
||||||
constructor(x: number) { this.g = x }
|
|
||||||
/** M s */
|
|
||||||
static s(y: number): MI { return {k: y} }
|
|
||||||
/** M m */
|
|
||||||
m(z: number): MI { return {k: z} }
|
|
||||||
|
|
||||||
/** M p */
|
|
||||||
private p(z: number): MI { return {k: z} }
|
|
||||||
}
|
|
||||||
|
|
||||||
type MT = MC
|
|
||||||
|
|
||||||
export const c = new MC(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** HM */
|
|
||||||
module HM {
|
|
||||||
/** HMI */
|
|
||||||
interface HMI {
|
|
||||||
/** HM k */
|
|
||||||
k: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/** HMC */
|
|
||||||
class HMC {
|
|
||||||
/** HM f */
|
|
||||||
f = 1
|
|
||||||
/** HM g */
|
|
||||||
g: number
|
|
||||||
/** HM new */
|
|
||||||
constructor(x: number) { this.g = x }
|
|
||||||
/** HM s */
|
|
||||||
static s(y: number): HMI { return {k: y} }
|
|
||||||
/** HM m */
|
|
||||||
m(z: number): HMI { return {k: z} }
|
|
||||||
}
|
|
||||||
|
|
||||||
type HMT = HMC
|
|
||||||
|
|
||||||
const c = new HMC(1)
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/** The awesome iface
|
|
||||||
|
|
||||||
1 // => 1
|
|
||||||
|
|
||||||
This is not indentend
|
|
||||||
*/
|
|
||||||
export interface B {
|
|
||||||
b: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class C {
|
|
||||||
/**
|
|
||||||
|
|
||||||
1 // => 1
|
|
||||||
much.to.test()
|
|
||||||
yes()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
more stuff*/
|
|
||||||
static boo() { return 1 }
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,417 +0,0 @@
|
||||||
import * as babylon from 'babylon'
|
|
||||||
import * as babel from 'babel-types'
|
|
||||||
import generate from 'babel-generator'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
import * as util from 'util'
|
|
||||||
|
|
||||||
util.inspect.defaultOptions.depth = 5
|
|
||||||
util.inspect.defaultOptions.colors = true
|
|
||||||
const pp = (x: any) => (console.dir(x), console.log())
|
|
||||||
|
|
||||||
const opts: babylon.BabylonOptions = {plugins: [
|
|
||||||
'estree' ,
|
|
||||||
'jsx' ,
|
|
||||||
'flow' ,
|
|
||||||
'classConstructorCall' ,
|
|
||||||
'doExpressions' ,
|
|
||||||
'objectRestSpread' ,
|
|
||||||
'decorators' ,
|
|
||||||
'classProperties' ,
|
|
||||||
'exportExtensions' ,
|
|
||||||
'asyncGenerators' ,
|
|
||||||
'functionBind' ,
|
|
||||||
'functionSent' ,
|
|
||||||
'dynamicImport']}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const is_doctest = (s: string) => s.match(/\/\/[ \t]*=>/) != null
|
|
||||||
const doctest_rhs = (s: string) => s.match(/^\s*[ \t]*=>((.|\n)*)$/m)
|
|
||||||
|
|
||||||
interface Equality {
|
|
||||||
tag: '==',
|
|
||||||
lhs: string,
|
|
||||||
rhs: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Statement {
|
|
||||||
tag: 'Statement'
|
|
||||||
stmt: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Script = (Statement | Equality)[]
|
|
||||||
|
|
||||||
export function test(s: string): Script {
|
|
||||||
const lin = (ast: babel.Node) => generate(ast ,{comments: false, compact: true}).code
|
|
||||||
const ast = babylon.parse(s, opts)
|
|
||||||
return ast.program.body.map((stmt): Statement | Equality => {
|
|
||||||
const comment = (stmt.trailingComments || [{value: ''}])[0].value
|
|
||||||
const rhs = doctest_rhs(comment)
|
|
||||||
if (babel.isExpressionStatement(stmt) && rhs) {
|
|
||||||
const rhs = babylon.parseExpression(comment.replace(/^\s*=>/, ''))
|
|
||||||
return {
|
|
||||||
tag: '==',
|
|
||||||
lhs: lin(stmt.expression),
|
|
||||||
rhs: lin(rhs),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {tag: 'Statement', stmt: lin(stmt)}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tests(docstring: string): Script[] {
|
|
||||||
const out = [] as Script[]
|
|
||||||
docstring.split(/\n\n+/m).forEach(s => {
|
|
||||||
if (is_doctest(s)) {
|
|
||||||
out.push(test(s))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface Comment {
|
|
||||||
comment: string,
|
|
||||||
context: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Comments(s: string): Comment[] {
|
|
||||||
const out: Comment[] = []
|
|
||||||
function add_comment(c: babel.Comment, context: string | null) {
|
|
||||||
out.push({comment: c.value, context})
|
|
||||||
}
|
|
||||||
|
|
||||||
const ast = babylon.parse(s, opts)
|
|
||||||
|
|
||||||
traverse(ast, node => {
|
|
||||||
let context: null | string = null
|
|
||||||
/*
|
|
||||||
if (babel.isDeclaration(node)) {
|
|
||||||
util.inspect.defaultOptions.depth = 1
|
|
||||||
pp({declaration: node})
|
|
||||||
}
|
|
||||||
if (babel.isMethod(node)) {
|
|
||||||
util.inspect.defaultOptions.depth = 1
|
|
||||||
pp({method: node})
|
|
||||||
}
|
|
||||||
if (isObject(node) && 'type' in node && node.type == 'MethodDefinition') {
|
|
||||||
util.inspect.defaultOptions.depth = 2
|
|
||||||
pp({methodDefn: node})
|
|
||||||
}
|
|
||||||
if (isObject(node) && 'type' in node && node.type == 'ObjectProperty') {
|
|
||||||
util.inspect.defaultOptions.depth = 2
|
|
||||||
pp({objProp: node})
|
|
||||||
}
|
|
||||||
if (isObject(node) && 'type' in node && node.type == 'ObjectMethod') {
|
|
||||||
util.inspect.defaultOptions.depth = 2
|
|
||||||
pp({objMethod: node})
|
|
||||||
}
|
|
||||||
if (isObject(node) && 'type' in node && node.type == 'ObjectExpression') {
|
|
||||||
util.inspect.defaultOptions.depth = 5
|
|
||||||
pp({objExpr: node})
|
|
||||||
}
|
|
||||||
if (isObject(node) && 'type' in node && node.type == 'Property') {
|
|
||||||
util.inspect.defaultOptions.depth = 5
|
|
||||||
pp({property: node})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// context = node as any
|
|
||||||
function has_key(x: any): x is {key: babel.Identifier} {
|
|
||||||
return isObject(x) && 'key' in x && babel.isIdentifier((x as any).key)
|
|
||||||
}
|
|
||||||
function has_id(x: any): x is {id: babel.Identifier} {
|
|
||||||
return isObject(x) && 'id' in x && babel.isIdentifier((x as any).id)
|
|
||||||
}
|
|
||||||
if (babel.isVariableDeclaration(node)) {
|
|
||||||
const ds = node.declarations
|
|
||||||
if (ds.length == 1) {
|
|
||||||
const d = ds[0]
|
|
||||||
if (has_id(d)) {
|
|
||||||
context = d.id.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (has_id(node)) {
|
|
||||||
context = node.id.name
|
|
||||||
} else if (has_key(node)) {
|
|
||||||
context = node.key.name
|
|
||||||
}
|
|
||||||
if (isObject(node)) {
|
|
||||||
function add_comments(s: string) {
|
|
||||||
if (s in node && Array.isArray(node[s])) {
|
|
||||||
(node[s] as any[]).forEach(c => {
|
|
||||||
if ('type' in c) {
|
|
||||||
if (c.type == 'CommentBlock' || c.type == 'CommentLine') {
|
|
||||||
add_comment(c, context)
|
|
||||||
if (context == null) {
|
|
||||||
// pp({c, node})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isObject(node)) {
|
|
||||||
add_comments('leadingComments')
|
|
||||||
add_comments('innerComments')
|
|
||||||
// add_comments('trailingComments')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function isObject(x: any): x is object {
|
|
||||||
return x !== null && typeof x === 'object' && !Array.isArray(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
function traverse(x: any, f: (x: any) => void): void {
|
|
||||||
f(x)
|
|
||||||
if (Array.isArray(x)) {
|
|
||||||
x.map(y => traverse(y, f))
|
|
||||||
}
|
|
||||||
if (isObject(x)) {
|
|
||||||
for (const k in x) {
|
|
||||||
traverse((x as any)[k], f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
false && pp(Comments(`/** test */ function f(x: string): number { return 1 }
|
|
||||||
|
|
||||||
class Apa {
|
|
||||||
/** something something */
|
|
||||||
|
|
||||||
/** attached to nothing! */
|
|
||||||
|
|
||||||
/** returns important stuff
|
|
||||||
|
|
||||||
j(5) // => 6
|
|
||||||
|
|
||||||
j(9) // => 10
|
|
||||||
*/
|
|
||||||
j(x: number) {
|
|
||||||
/** important stuff */
|
|
||||||
return x + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface B {
|
|
||||||
/** x docstring */
|
|
||||||
x: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/** u docstring */
|
|
||||||
const u = {
|
|
||||||
/** ux docstring */
|
|
||||||
ux: 1
|
|
||||||
}
|
|
||||||
`))
|
|
||||||
|
|
||||||
function script(filename: string, s: string): string[] {
|
|
||||||
return []
|
|
||||||
/*
|
|
||||||
const pwoc = ts.createPrinter({removeComments: true})
|
|
||||||
const f = ts.createSourceFile('_doctest_' + filename, s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS)
|
|
||||||
const out =
|
|
||||||
f.statements.map(
|
|
||||||
(now, i) => {
|
|
||||||
if (ts.isExpressionStatement(now)) {
|
|
||||||
const next = f.statements[i+1] // zip with next
|
|
||||||
const [a, z] = next ? [next.pos, next.end] : [now.end, f.end]
|
|
||||||
const after = f.text.slice(a, z)
|
|
||||||
const m = doctest_rhs(after)
|
|
||||||
if (m && m[1]) {
|
|
||||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, now.expression, f)
|
|
||||||
const rhs = m[1].trim()
|
|
||||||
return 't.deepEqual(' + lhs + ', ' + rhs + ', ' + JSON.stringify(rhs) + ')'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pwoc.printNode(ts.EmitHint.Unspecified, now, f)
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
const filename = 'unk.ts'
|
|
||||||
|
|
||||||
r.comments.map(d => {
|
|
||||||
d.value.split(/\n\n+/m).map(s => {
|
|
||||||
let tests = 0
|
|
||||||
if (is_doctest(s)) {
|
|
||||||
// todo: typecheck s now
|
|
||||||
const name = 'unk'
|
|
||||||
console.log(
|
|
||||||
'test(' + JSON.stringify(name + ' ' + ++tests) + ', t => {',
|
|
||||||
...script(filename, s).map(l => ' ' + l),
|
|
||||||
'})',
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
pp(r.program)
|
|
||||||
pp(r.comments)
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
|
|
||||||
function script(filename: string, s: string): string[] {
|
|
||||||
const pwoc = ts.createPrinter({removeComments: true})
|
|
||||||
const f = ts.createSourceFile('_doctest_' + filename, s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS)
|
|
||||||
const out =
|
|
||||||
f.statements.map(
|
|
||||||
(now, i) => {
|
|
||||||
if (ts.isExpressionStatement(now)) {
|
|
||||||
const next = f.statements[i+1] // zip with next
|
|
||||||
const [a, z] = next ? [next.pos, next.end] : [now.end, f.end]
|
|
||||||
const after = f.text.slice(a, z)
|
|
||||||
const m = doctest_rhs(after)
|
|
||||||
if (m && m[1]) {
|
|
||||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, now.expression, f)
|
|
||||||
const rhs = m[1].trim()
|
|
||||||
return 't.deepEqual(' + lhs + ', ' + rhs + ', ' + JSON.stringify(rhs) + ')'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pwoc.printNode(ts.EmitHint.Unspecified, now, f)
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_script_one(filename: string, d: Def): string[] {
|
|
||||||
const out = [] as string[]
|
|
||||||
let tests = 0
|
|
||||||
d.doc.split(/\n\n+/m).map(s => {
|
|
||||||
if (is_doctest(s)) {
|
|
||||||
// todo: typecheck s now
|
|
||||||
out.push(
|
|
||||||
'test(' + JSON.stringify(d.name + ' ' + ++tests) + ', t => {',
|
|
||||||
...script(filename, s).map(l => ' ' + l),
|
|
||||||
'})',
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_script(top: Top) {
|
|
||||||
return ["import {test} from 'ava'"].concat(...top.map(
|
|
||||||
({filename, defs}) => walk(defs, (d) => test_script_one(filename, d)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function prettyKind(kind: string) {
|
|
||||||
return kind.replace('Declaration', '').toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toc_one(def: Def, i: number): string[] {
|
|
||||||
if (def.exported || i > 0) {
|
|
||||||
return [
|
|
||||||
replicate(i, ' ').join('') +
|
|
||||||
'* ' +
|
|
||||||
(def.children.length == 0 ? '' : (prettyKind(def.kind) + ' ')) +
|
|
||||||
def.name
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toc(top: Top): string[] {
|
|
||||||
return flatten(top.map(({defs}) => walk(defs, toc_one)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function doc_one(def: Def, i: number): string[] {
|
|
||||||
const out = [] as string[]
|
|
||||||
if (def.exported || i > 0) {
|
|
||||||
let indent = ''
|
|
||||||
if (def.children.length == 0) {
|
|
||||||
//const method = (def.kind == 'MethodDeclaration') ? 'method ' : ''
|
|
||||||
out.push('* ' + '**' + def.name + '**: `' + def.type + '`')
|
|
||||||
indent = ' '
|
|
||||||
} else {
|
|
||||||
out.push('### ' + prettyKind(def.kind) + ' ' + def.name)
|
|
||||||
}
|
|
||||||
def.doc.split(/\n\n+/).forEach(s => {
|
|
||||||
out.push('')
|
|
||||||
if (is_doctest(s)) {
|
|
||||||
out.push(indent + '```typescript')
|
|
||||||
}
|
|
||||||
const lines = s.split('\n')
|
|
||||||
lines.forEach(line => out.push(indent + line))
|
|
||||||
if (is_doctest(s)) {
|
|
||||||
out.push(indent + '```')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function doc(top: Top) {
|
|
||||||
return flatten(top.map(({defs}) => walk(defs, doc_one)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const filenames = [] as string[]
|
|
||||||
const argv = process.argv.slice(2)
|
|
||||||
const outputs = [] as ((top: Top) => string[])[]
|
|
||||||
|
|
||||||
{
|
|
||||||
let program: ts.Program
|
|
||||||
let verbose = false
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
|
||||||
const arg = argv[i]
|
|
||||||
if (arg == '-t' || arg == '--test-script') {
|
|
||||||
outputs.push(test_script)
|
|
||||||
} else if (arg == '-d' || arg == '--doc') {
|
|
||||||
outputs.push(doc)
|
|
||||||
} else if (arg == '--toc' || arg == '--toc') {
|
|
||||||
outputs.push(toc)
|
|
||||||
} else if (arg == '-i' || arg == '--include') {
|
|
||||||
outputs.push(_top => [fs.readFileSync(argv[++i]).toString()])
|
|
||||||
} else if (arg == '-s' || arg == '--string') {
|
|
||||||
outputs.push(_top => [argv[++i]])
|
|
||||||
} else {
|
|
||||||
filenames.push(arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outputs.length == 0) {
|
|
||||||
console.log(`typescript-doctests <args>
|
|
||||||
Each entry in <args> may be:
|
|
||||||
[-t|--test-script] // write tape test script on stdout
|
|
||||||
[-d|--doc] // write markdown api documentation on stdout
|
|
||||||
[--toc] // write markdown table of contents on stdout
|
|
||||||
[-i|--include] FILENAME // write the contents of a file on stdout
|
|
||||||
[-s|--string] STRING // write a string literally on stdout
|
|
||||||
FILENAME // typescript files to look for docstrings in
|
|
||||||
|
|
||||||
Example usages:
|
|
||||||
|
|
||||||
typescript-doctests src/*.ts -s 'import * as App from "../src/App"' -t > test/App.doctest.ts
|
|
||||||
|
|
||||||
typescript-doctests src/*.ts -i Header.md --toc --doc -i Footer.md > README.md
|
|
||||||
`)
|
|
||||||
process.exit(1)
|
|
||||||
} else {
|
|
||||||
program = ts.createProgram(filenames, {
|
|
||||||
target: ts.ScriptTarget.ES5,
|
|
||||||
module: ts.ModuleKind.CommonJS
|
|
||||||
})
|
|
||||||
const top = generateDocumentation(program, filenames)
|
|
||||||
|
|
||||||
outputs.forEach(m => m(top).forEach(line => console.log(line)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,46 +0,0 @@
|
||||||
import * as ts from 'typescript'
|
|
||||||
|
|
||||||
const pwoc = ts.createPrinter({removeComments: true})
|
|
||||||
|
|
||||||
function script(s: string): string {
|
|
||||||
const f = ts.createSourceFile('test.ts', s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS)
|
|
||||||
const out =
|
|
||||||
f.statements.map(
|
|
||||||
(now, i) => {
|
|
||||||
if (ts.isExpressionStatement(now)) {
|
|
||||||
const next = f.statements[i+1] // zip with next
|
|
||||||
const [a, z] = next ? [next.pos, next.end] : [now.end, f.end]
|
|
||||||
const after = f.text.slice(a, z)
|
|
||||||
const m = after.match(/^\s*\/\/[ \t]*=>([^\n]*)/m)
|
|
||||||
if (m && m[1]) {
|
|
||||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, now.expression, f)
|
|
||||||
const rhs = m[1].trim()
|
|
||||||
return 'assert.deepEqual(' + lhs + ', ' + rhs + ', ' + JSON.stringify(rhs) + ')'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pwoc.printNode(ts.EmitHint.Unspecified, now, f)
|
|
||||||
})
|
|
||||||
return out.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
const s = `
|
|
||||||
const a = 1
|
|
||||||
a
|
|
||||||
// one more
|
|
||||||
// => 1
|
|
||||||
let b = 2
|
|
||||||
a + 1 // => 2
|
|
||||||
// that's all
|
|
||||||
a + b
|
|
||||||
// that's all
|
|
||||||
// => 3
|
|
||||||
function apa(bepa) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
a++
|
|
||||||
b++
|
|
||||||
// hehe // => 5
|
|
||||||
a // => 4
|
|
||||||
`
|
|
||||||
|
|
||||||
console.log(script(s))
|
|
Loading…
Add table
Reference in a new issue