Refactor
This commit is contained in:
parent
a93318a0b3
commit
2bc463153d
2 changed files with 133 additions and 97 deletions
|
@ -7,6 +7,7 @@
|
||||||
"typescript-doctest": "src/main.js"
|
"typescript-doctest": "src/main.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"preinstall": "tsc",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -25,7 +26,6 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/danr/typescript-doctest#readme",
|
"homepage": "https://github.com/danr/typescript-doctest#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs": "0.0.1-security",
|
|
||||||
"typescript": "^2.6.1"
|
"typescript": "^2.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
170
src/main.ts
170
src/main.ts
|
@ -1,8 +1,9 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import * as ts from 'typescript'
|
import * as ts from 'typescript'
|
||||||
import * as fs from "fs"
|
import * as fs from 'fs'
|
||||||
|
|
||||||
const is_doctest = (s: string) => s.match( /\/\/[ \t]*=>/) != null
|
const is_doctest = (s: string) => s.match( /\/\/[ \t]*=>/) != null
|
||||||
|
const doctest_rhs = (s: string) => s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m)
|
||||||
|
|
||||||
function replicate<A>(i: number, x: A): A[] {
|
function replicate<A>(i: number, x: A): A[] {
|
||||||
const out = [] as A[]
|
const out = [] as A[]
|
||||||
|
@ -10,6 +11,14 @@ function replicate<A>(i: number, x: A): A[] {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flatMap<A, B>(xs: A[], f: (a: A) => B[]): B[] {
|
||||||
|
return ([] as B[]).concat(...xs.map(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatten<A>(xss: A[][]): A[] {
|
||||||
|
return ([] as A[]).concat(...xss)
|
||||||
|
}
|
||||||
|
|
||||||
type Top = {filename: string, defs: Defs}[]
|
type Top = {filename: string, defs: Defs}[]
|
||||||
|
|
||||||
type Defs = Def[]
|
type Defs = Def[]
|
||||||
|
@ -25,25 +34,18 @@ interface Def {
|
||||||
kind: string,
|
kind: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
function walk(defs: Defs, f: (def: Def, d: number) => void, d0: number = 0): void {
|
function walk<A>(defs: Defs, f: (def: Def, d: number) => A[], d0: number = 0): A[] {
|
||||||
defs.forEach(d => (f(d, d0), walk(d.children, f, d0 + 1)))
|
return flatten(defs.map(d => f(d, d0).concat(...walk(d.children, f, d0 + 1))))
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatMap<A, B>(xs: A[], f: (a: A) => B[]): B[] {
|
/** Generate documentation for all classes in a set of .ts files
|
||||||
return ([] as B[]).concat(...xs.map(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatten<A>(xss: A[][]): A[] {
|
Adapted from TS wiki about using the compiler API */
|
||||||
return ([] as A[]).concat(...xss)
|
function generateDocumentation(program: ts.Program, filenames: string[]): Top {
|
||||||
}
|
|
||||||
|
|
||||||
/** Generate documentation for all classes in a set of .ts files */
|
|
||||||
function generateDocumentation(fileNames: string[], options: ts.CompilerOptions): Top {
|
|
||||||
const program = ts.createProgram(fileNames, options)
|
|
||||||
const checker = program.getTypeChecker()
|
const checker = program.getTypeChecker()
|
||||||
const printer = ts.createPrinter()
|
const printer = ts.createPrinter()
|
||||||
return program.getSourceFiles()
|
return program.getSourceFiles()
|
||||||
.filter(file => -1 != fileNames.indexOf(file.fileName))
|
.filter(file => -1 != filenames.indexOf(file.fileName))
|
||||||
.map(file => ({
|
.map(file => ({
|
||||||
filename: file.fileName,
|
filename: file.fileName,
|
||||||
defs: flatten(file.statements.map(visits))
|
defs: flatten(file.statements.map(visits))
|
||||||
|
@ -128,10 +130,10 @@ function generateDocumentation(fileNames: string[], options: ts.CompilerOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function script(program: ts.Program, filename: string, s: string): string[] {
|
||||||
const pwoc = ts.createPrinter({removeComments: true})
|
const pwoc = ts.createPrinter({removeComments: true})
|
||||||
|
const f = ts.createSourceFile('_doctest_' + filename, s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS)
|
||||||
function script(s: string): string[] {
|
ts.getPreEmitDiagnostics(program, f)
|
||||||
const f = ts.createSourceFile('test.ts', s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS)
|
|
||||||
const out =
|
const out =
|
||||||
f.statements.map(
|
f.statements.map(
|
||||||
(now, i) => {
|
(now, i) => {
|
||||||
|
@ -139,7 +141,7 @@ function script(s: string): string[] {
|
||||||
const next = f.statements[i+1] // zip with next
|
const next = f.statements[i+1] // zip with next
|
||||||
const [a, z] = next ? [next.pos, next.end] : [now.end, f.end]
|
const [a, z] = next ? [next.pos, next.end] : [now.end, f.end]
|
||||||
const after = f.text.slice(a, z)
|
const after = f.text.slice(a, z)
|
||||||
const m = after.match(/^\s*\/\/[ \t]*=>([^\n]*)/m)
|
const m = doctest_rhs(after)
|
||||||
if (m && m[1]) {
|
if (m && m[1]) {
|
||||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, now.expression, f)
|
const lhs = pwoc.printNode(ts.EmitHint.Expression, now.expression, f)
|
||||||
const rhs = m[1].trim()
|
const rhs = m[1].trim()
|
||||||
|
@ -151,93 +153,127 @@ function script(s: string): string[] {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = ["import * as test from 'tape'"]
|
function test_script_one(program: ts.Program, filename: string, d: Def): string[] {
|
||||||
const filenames = [] as string[]
|
const out = [] as string[]
|
||||||
let fileout = null
|
|
||||||
const argv = process.argv
|
|
||||||
for (let i = 2; i < argv.length; i++) {
|
|
||||||
const arg = argv[i]
|
|
||||||
if (arg == '-o') {
|
|
||||||
fileout = argv[++i]
|
|
||||||
} else if (arg == '-i') {
|
|
||||||
headers.push(argv[++i])
|
|
||||||
} else {
|
|
||||||
filenames.push(arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const top = generateDocumentation(filenames, {
|
|
||||||
target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
if (fileout != null) {
|
|
||||||
const out = headers.slice()
|
|
||||||
|
|
||||||
top.forEach(({filename, defs}) => {
|
|
||||||
walk(defs, d => {
|
|
||||||
let tests = 0
|
let tests = 0
|
||||||
d.doc.split(/\n\n+/m).map(s => {
|
d.doc.split(/\n\n+/m).map(s => {
|
||||||
if (is_doctest(s)) {
|
if (is_doctest(s)) {
|
||||||
|
// todo: typecheck s now
|
||||||
out.push(
|
out.push(
|
||||||
'test(' + JSON.stringify(d.name + ' ' + ++tests) + ', assert => {',
|
'test(' + JSON.stringify(d.name + ' ' + ++tests) + ', assert => {',
|
||||||
...script(s).map(l => ' ' + l),
|
...script(program, filename, s).map(l => ' ' + l),
|
||||||
' assert.end()',
|
' assert.end()',
|
||||||
'})',
|
'})',
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
return out
|
||||||
})
|
|
||||||
|
|
||||||
fs.writeFileSync(fileout, out.join('\n'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const readme = [] as string[]
|
function test_script(program: ts.Program, top: Top) {
|
||||||
|
return ["import * as test from 'tape'"].concat(...top.map(
|
||||||
|
({filename, defs}) => walk(defs, (d) => test_script_one(program, filename, d)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
readme.push('## API overview')
|
|
||||||
|
|
||||||
function prettyKind(kind: string) {
|
function prettyKind(kind: string) {
|
||||||
return kind.replace('Declaration', '').toLowerCase()
|
return kind.replace('Declaration', '').toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
top.forEach(({defs}) => {
|
function toc_one(def: Def, i: number): string[] {
|
||||||
walk(defs, (def, i) => {
|
|
||||||
if (def.exported || i > 0) {
|
if (def.exported || i > 0) {
|
||||||
readme.push(
|
return [
|
||||||
replicate(i, ' ').join('') +
|
replicate(i, ' ').join('') +
|
||||||
'* ' +
|
'* ' +
|
||||||
(def.children.length == 0 ? '' : (prettyKind(def.kind) + ' ')) +
|
(def.children.length == 0 ? '' : (prettyKind(def.kind) + ' ')) +
|
||||||
def.name)
|
def.name
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
top.forEach(({defs}) =>
|
function toc(top: Top): string[] {
|
||||||
walk(defs, (def, i) => {
|
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) {
|
if (def.exported || i > 0) {
|
||||||
let indent = ''
|
let indent = ''
|
||||||
if (def.children.length == 0) {
|
if (def.children.length == 0) {
|
||||||
//const method = (def.kind == 'MethodDeclaration') ? 'method ' : ''
|
//const method = (def.kind == 'MethodDeclaration') ? 'method ' : ''
|
||||||
readme.push('* ' + '**' + def.name + '**: `' + def.type + '`')
|
out.push('* ' + '**' + def.name + '**: `' + def.type + '`')
|
||||||
indent = ' '
|
indent = ' '
|
||||||
} else {
|
} else {
|
||||||
readme.push('### ' + prettyKind(def.kind) + ' ' + def.name)
|
out.push('### ' + prettyKind(def.kind) + ' ' + def.name)
|
||||||
}
|
}
|
||||||
def.doc.split(/\n\n+/).forEach(s => {
|
def.doc.split(/\n\n+/).forEach(s => {
|
||||||
readme.push('')
|
out.push('')
|
||||||
if (is_doctest(s)) {
|
if (is_doctest(s)) {
|
||||||
readme.push(indent + '```typescript')
|
out.push(indent + '```typescript')
|
||||||
}
|
}
|
||||||
readme.push(indent + s.trim().split('\n').join('\n' + indent))
|
out.push(indent + s.trim().split('\n').join('\n' + indent))
|
||||||
if (is_doctest(s)) {
|
if (is_doctest(s)) {
|
||||||
readme.push(indent + '```')
|
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
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i]
|
||||||
|
if (arg == '-t' || arg == '--test-script') {
|
||||||
|
outputs.push(top => test_script(program, top))
|
||||||
|
} 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
|
||||||
|
`)
|
||||||
|
} else {
|
||||||
|
program = ts.createProgram(filenames, {
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
module: ts.ModuleKind.CommonJS
|
||||||
})
|
})
|
||||||
)
|
const top = generateDocumentation(program, filenames)
|
||||||
|
|
||||||
console.log(readme.join('\n'))
|
|
||||||
|
|
||||||
|
outputs.forEach(m => m(top).forEach(line => console.log(line)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue