Add support for a title, add support for tags in docstrings, expose private methods in doctests to make them testable

This commit is contained in:
Pieter Vander Vennet 2022-03-17 20:38:01 +01:00
parent 1812b4e0e8
commit e8ddc329e8
3 changed files with 75 additions and 13 deletions

18
examples/example0.ts Normal file
View file

@ -0,0 +1,18 @@
export default class Example0 {
/**
* Gets the field doubled
* @example xyz
*
* // Should equal 42
* Example0.get() // => 42
*
* Example0.get() + 1 // => 43
*/
private static get(){
return 42
}
}

View file

@ -1,7 +1,7 @@
{
"$schema": "http://json.schemastore.org/package",
"name": "doctest-ts",
"version": "0.5.0",
"version": "0.6.0",
"description": "doctest support for typescript",
"main": "src/main.ts",
"bin": {
@ -11,6 +11,7 @@
"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",
"debug": "ts-node src/main.ts --mocha examples/example0.ts",
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*"
},
"repository": {

View file

@ -1,4 +1,5 @@
import * as ts from 'typescript'
import {SyntaxKind} from 'typescript'
import * as fs from 'fs'
import * as path from 'path'
@ -30,7 +31,6 @@ export interface Comment {
export function Comments(s: string): Comment[] {
const out: Comment[] = []
function add_comment(c: string, context: string | null) {}
function traverse(node: ts.Node) {
const jsdocs = (node as any).jsDoc || []
@ -50,8 +50,22 @@ export function Comments(s: string): Comment[] {
}
}
jsdocs.forEach((doc: ts.JSDoc) => {
out.push({comment: doc.comment || '', context})
out.push({comment: doc.comment || '', context});
// 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 => {
out.push({comment: tag.comment || '', context});
});
}
/*(doc.tags || []).forEach(tag => {
console.log(tag)
})*/
})
}
ts.forEachChild(node, traverse)
}
@ -110,11 +124,18 @@ export function extractScript(s: string): Script {
})
}
export function extractScripts(docstring: string): Script[] {
const out = [] as Script[]
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)) {
out.push(extractScript(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
@ -124,7 +145,7 @@ export function extractScripts(docstring: string): Script[] {
// Showing test scripts
export interface ShowScript {
showImports: string
showScript(script: Script, c: Context): string
showScript(script: Script, c: Context, name?: string): string
}
/** show("hello") // => '"hello"' */
@ -136,7 +157,7 @@ export function showContext(c: Context) {
return show(c || 'doctest')
}
function tapeOrAVA(script: Script, c: Context, before_end = (t: string) => '') {
function tapeOrAVA(script: Script, c: Context, name: string | undefined, before_end = (t: string) => '') {
const t = `t`
const body = script
.map(s => {
@ -155,7 +176,7 @@ function tapeOrAVA(script: Script, c: Context, before_end = (t: string) => '') {
})`
}
const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c) => {
const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c, name) => {
const body = script
.map(s => {
if (s.tag == 'Statement') {
@ -169,7 +190,7 @@ const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c) => {
return `
describe(${showContext(c)}, () => {
it(${showContext(c)}, () => {${body}})
it(${show(name) || showContext(c)}, () => {${body}})
})
`
}
@ -182,7 +203,7 @@ export const showScriptInstances: Record<string, ShowScript> = {
tape: {
showImports: 'import * as __test from "tape"',
showScript: (s, c) => tapeOrAVA(s, c, t => `\n;${t}.end()`),
showScript: (s, c, name) => tapeOrAVA(s, c, name, t => `\n;${t}.end()`),
},
mocha: {
@ -196,12 +217,34 @@ export const showScriptInstances: Record<string, ShowScript> = {
},
}
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: true})
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) {
@ -211,7 +254,7 @@ export function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
if (mode == 'watch') {
console.log(outfile)
}
fs.writeFileSync(outfile, buffer + '\n' + d.showImports + '\n' + tests.join('\n'))
fs.writeFileSync(outfile, withoutPrivates + '\n' + d.showImports + '\n' + tests.join('\n'))
}
}
@ -219,7 +262,7 @@ 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, c.context))
out.push(d.showScript(script.script, c.context, script.name))
}
}
return out