This commit is contained in:
Emiliano Heyns
2021-11-03 10:34:42 +01:00
parent 1c7dad05d5
commit 7db78b31bd
4 changed files with 198 additions and 177 deletions

2
babel

Submodule babel updated: 187ac9b061...8d22061b5a

143
setup/api-extractor.ts Executable file
View File

@@ -0,0 +1,143 @@
import * as ts from 'typescript'
import * as fs from 'fs'
export type Parameter = {
name: string
default: string | number | boolean
}
export type Method = {
doc: string
parameters: Parameter[]
schema: any
}
export class API {
private ast: ts.SourceFile
public classes: Record<string, Record<string, Method>> = {}
constructor(filename: string) {
this.ast = ts.createSourceFile(filename, fs.readFileSync(filename, 'utf8'), ts.ScriptTarget.Latest)
this.ast.forEachChild(stmt => {
if (ts.isClassDeclaration(stmt)) {
this.ClassDeclaration(stmt)
}
})
}
private ClassDeclaration(cls: ts.ClassDeclaration): void {
const className: string = cls.name.getText(this.ast)
if (!className) return
cls.forEachChild(member => {
if (ts.isMethodDeclaration(member)) this.MethodDeclaration(className, member)
})
}
private MethodDeclaration(className: string, method: ts.MethodDeclaration): void {
const methodName: string = method.name.getText(this.ast)
if (!methodName) return
const comment_ranges = ts.getLeadingCommentRanges(this.ast.getFullText(), method.getFullStart())
if (!comment_ranges) return
let comment = this.ast.getFullText().slice(comment_ranges[0].pos, comment_ranges[0].end)
if (!comment.startsWith('/**')) return
comment = comment.replace(/^\/\*\*/, '').replace(/\*\/$/, '').trim().split('\n').map(line => line.replace(/^\s*[*]\s*/, '')).join('\n').replace(/\n+/g, newlines => newlines.length > 1 ? '\n\n' : ' ')
if (!this.classes[className]) this.classes[className] = {}
this.classes[className][methodName] = {
doc: comment,
parameters: [],
schema: {
type: 'object',
properties: {},
additionalProperties: false,
required: [],
},
}
method.forEachChild(param => {
if (ts.isParameter(param)) this.ParameterDeclaration(this.classes[className][methodName], param)
})
}
private ParameterDeclaration(method: Method, param: ts.ParameterDeclaration) {
const p: Parameter = {
name: param.name.getText(this.ast),
default: this.initializer(param.initializer),
}
method.parameters.push(p)
method.schema.properties[p.name] = param.type ? this.schema(param.type) : { type: typeof p.default }
if (!param.initializer && !param.questionToken) method.schema.required.push(p.name)
}
private initializer(init): string | number {
if (!init) return undefined
switch (init.kind) {
case ts.SyntaxKind.StringLiteral:
return init.text as string
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.FirstLiteralToken: // https://github.com/microsoft/TypeScript/issues/18062
return parseFloat(init.getText(this.ast))
default:
throw new Error(`Unexpected kind ${init.kind} ${ts.SyntaxKind[init.kind]} of initializer ${JSON.stringify(init)}`)
}
}
private schema(type: ts.TypeNode): any {
switch (type.kind) {
case ts.SyntaxKind.UnionType:
return this.UnionType(type as unknown as ts.UnionType)
case ts.SyntaxKind.LiteralType:
return this.LiteralType(type as unknown as ts.LiteralTypeNode)
case ts.SyntaxKind.StringKeyword:
return { type: 'string' }
case ts.SyntaxKind.BooleanKeyword:
return { type: 'boolean' }
// case ts.SyntaxKind.TypeReference:
// return null
case ts.SyntaxKind.NumberKeyword:
return { type: 'number' }
default:
throw {...type, kindName: ts.SyntaxKind[type.kind] } // eslint-disable-line no-throw-literal
}
}
private LiteralType(type: ts.LiteralTypeNode): any {
let value: string = type.literal.getText(this.ast)
if (ts.isStringLiteral(type.literal)) value = JSON.parse(value)
return { const: value }
}
private UnionType(type: ts.UnionType): any {
const types = type.types.map((t: ts.Type) => this.schema(t as unknown as ts.TypeNode)) // eslint-disable-line @typescript-eslint/no-unsafe-return
if (types.length === 1) return types[0]
const consts = []
const other = types.filter(t => {
if (typeof t.const === 'undefined') return true
consts.push(t.const)
return false
})
switch (consts.length) {
case 0:
case 1:
return { oneOf: types }
default:
return { oneOf : other.concat({ enum: consts }) }
}
}
}

View File

@@ -1,80 +1,63 @@
#!/usr/bin/env npx ts-node
/* eslint-disable prefer-template, @typescript-eslint/no-unsafe-return */
import * as ts from 'typescript'
import { Method, API } from './api-extractor'
import * as fs from 'fs'
const stringify = require('safe-stable-stringify')
import stringify from 'safe-stable-stringify'
const filename = 'content/key-manager/formatter.ts'
const ast = ts.createSourceFile(filename, fs.readFileSync(filename, 'utf8'), ts.ScriptTarget.Latest)
function kindName(node) {
node.kindName = ts.SyntaxKind[node.kind]
node.forEachChild(kindName)
}
kindName(ast)
function assert(cond, msg) {
if (!cond) throw new Error(msg)
}
const Method = new class {
class FormatterAPI {
private formatter: Record<string, Method>
public signature: Record<string, any> = {}
public doc: { function: Record<string, any>, filter: Record<string, any> } = { function: {}, filter: {} }
public doc: { function: Record<string, string>, filter: Record<string, string> } = { function: {}, filter: {} }
const2enum(types) {
const consts = []
const other = types.filter(type => {
if (typeof type.const === 'undefined') return true
consts.push(type.const)
return false
})
constructor(source: string) {
this.formatter = new API(source).classes.PatternFormatter
for (const [name, method] of Object.entries(this.formatter)) {
const kind = {$: 'function', _: 'filter'}[name[0]]
if (!kind) continue
switch (consts.length) {
case 0:
case 1:
return types
default:
return other.concat({ enum: consts })
}
}
this.signature[name] = JSON.parse(JSON.stringify({
parameters: method.parameters.map(p => p.name),
schema: method.schema,
}))
types(node) {
switch (node.kind) {
case ts.SyntaxKind.UnionType:
return { oneOf: this.const2enum(node.types.map(t => this.types(t)).filter(type => type)) }
case ts.SyntaxKind.LiteralType:
return { const: node.literal.text }
case ts.SyntaxKind.StringKeyword:
return { type: 'string' }
case ts.SyntaxKind.BooleanKeyword:
return { type: 'boolean' }
case ts.SyntaxKind.TypeReference:
return null
case ts.SyntaxKind.NumberKeyword:
return { type: 'number' }
default:
throw {...node, kindName: ts.SyntaxKind[node.kind] } // eslint-disable-line no-throw-literal
}
}
type(node) {
const types = this.types(node)
if (types.oneOf) {
assert(types.oneOf.length, types)
if (types.oneOf.length === 1) {
return types.oneOf[0]
let names = [ name.substr(1) ]
let name_edtr = ''
if (kind === 'function' && method.parameters.find(p => p.name === 'onlyEditors')) { // auth function
for (const [author, editor] of [['authors', 'editors'], ['auth.auth', 'edtr.edtr'], [ 'auth', 'edtr' ]]) {
if (names[0].startsWith(author)) {
names.push(name_edtr = names[0].replace(author, editor))
break
}
}
}
}
if (name_edtr) {
this.signature[name_edtr] = JSON.parse(JSON.stringify(this.signature[name]))
return types
for (const mname of [name, name_edtr]) {
this.signature[mname].schema.properties.onlyEditors = { const: mname === name_edtr }
}
}
names = names.map(n => n.replace(/__/g, '.').replace(/_/g, '-'))
if (kind === 'function') {
if (method.parameters.find(p => p.name === 'n')) names = names.map(n => `${n}N`)
if (method.parameters.find(p => p.name === 'm')) names = names.map(n => `${n}_M`)
}
let quoted = names.map(n => '`' + n + '`').join(' / ')
switch (kind) {
case 'function':
if (method.parameters.find(p => p.name === 'withInitials')) quoted += ', `+initials`'
if (method.parameters.find(p => p.name === 'joiner')) quoted += ', `+<joinchar>`'
break
case 'filter':
if (method.parameters.length) quoted += '=' + method.parameters.map(p => `${p.name}${method.schema.required.includes(p.name) ? '' : '?'} (${this.typedoc(method.schema.properties[p.name])})`).join(', ')
break
}
this.doc[kind][quoted] = method.doc
}
}
private typedoc(type): string {
@@ -84,115 +67,10 @@ const Method = new class {
if (type.enum) return type.enum.map(t => this.typedoc({ const: t })).join(' | ')
throw new Error(`no rule for ${JSON.stringify(type)}`)
}
initializer(init) {
if (!init) return undefined
switch (init.kind) {
case ts.SyntaxKind.StringLiteral:
return init.text
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.FirstLiteralToken: // https://github.com/microsoft/TypeScript/issues/18062
assert(!isNaN(parseFloat(init.text)), `${init.text} is not a number`)
return parseFloat(init.text)
default:
throw new Error(`Unexpected type ${init.type} of initializer ${JSON.stringify(init)}`)
}
}
add(method: ts.MethodDeclaration) {
const method_name: string = method.name.kind === ts.SyntaxKind.Identifier ? method.name.escapedText as string : ''
assert(method_name, method.name.getText(ast))
if (!method_name.match(/^[$_]/)) return
let method_name_edtr = ''
assert(!this.signature[method_name], `${method_name} already exists`)
const params = method.parameters.map(p => ({
name: p.name.kind === ts.SyntaxKind.Identifier ? (p.name.escapedText as string) : '',
type: p.type? this.type(p.type) : { type: typeof this.initializer(p.initializer) },
optional: !!(p.initializer || p.questionToken),
default: this.initializer(p.initializer),
}))
const kind = {$: 'function', _: 'filter'}[method_name[0]]
let names = [ method_name.substr(1) ]
if (params.find(p => p.name === 'onlyEditors')) {
for (const [author, editor] of [['authors', 'editors'], ['auth.auth', 'edtr.edtr'], [ 'auth', 'edtr' ]]) {
if (names[0].startsWith(author)) {
names.push(names[0].replace(author, editor))
method_name_edtr = editor
break
}
}
}
names = names.map(n => n.replace(/__/g, '.').replace(/_/g, '-'))
if (kind === 'function') {
if (params.find(p => p.name === 'n')) names = names.map(n => `${n}N`)
if (params.find(p => p.name === 'm')) names = names.map(n => `${n}_M`)
}
let quoted = names.map(n => '`' + n + '`').join(' / ')
switch (kind) {
case 'function':
if (params.find(p => p.name === 'withInitials')) quoted += ', `+initials`'
if (params.find(p => p.name === 'joiner')) quoted += ', `+<joinchar>`'
break
case 'filter':
if (params.length) quoted += '=' + params.map(p => `${p.name}${p.optional ? '?' : ''} (${this.typedoc(p.type)})`).join(', ')
break
}
const comment_ranges = ts.getLeadingCommentRanges(ast.getFullText(), method.getFullStart())
assert(comment_ranges, `${method_name} has no documentation`)
let comment = ast.getFullText().slice(comment_ranges[0].pos, comment_ranges[0].end)
assert(comment.startsWith('/**'), `comment for ${method_name} does not start with a doc-comment indicator`)
comment = comment.replace(/^\/\*\*/, '').replace(/\*\/$/, '').trim().split('\n').map(line => line.replace(/^\s*[*]\s*/, '')).join('\n').replace(/\n+/g, newlines => newlines.length > 1 ? '\n\n' : ' ')
this.doc[kind][quoted] = comment
const schema = {
type: 'object',
properties: {},
additionalProperties: false,
required: [],
}
names = []
for (const p of params) {
names.push(p.name)
if (!p.optional) schema.required.push(p.name)
schema.properties[p.name] = p.type
}
if (!schema.required.length) delete schema.required
this.signature[method_name] = JSON.parse(JSON.stringify({
arguments: names,
schema,
}))
if (method_name_edtr) {
this.signature[method_name_edtr] = JSON.parse(JSON.stringify({
arguments: names,
schema,
}))
for (const mname of [method_name, method_name_edtr]) {
this.signature[mname].schema.properties.onlyEditors = { const: mname === method_name_edtr }
}
}
}
}
ast.forEachChild((node: ts.Node) => {
// process only classes
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
const api = new FormatterAPI('content/key-manager/formatter.ts')
// get feautures of ClassDeclarations
const cls: ts.ClassDeclaration = node as ts.ClassDeclaration
// process class childs
cls.forEachChild((method: ts.Node) => {
if (method.kind === ts.SyntaxKind.MethodDeclaration) Method.add(method as ts.MethodDeclaration)
})
}
})
fs.writeFileSync('gen/key-formatter-methods.json', JSON.stringify(Method.signature, null, 2))
fs.writeFileSync('site/data/citekeyformatters/functions.json', stringify(Method.doc.function, null, 2))
fs.writeFileSync('site/data/citekeyformatters/filters.json', stringify(Method.doc.filter, null, 2))
fs.writeFileSync('gen/key-formatter-methods.json', JSON.stringify(api.signature, null, 2))
fs.writeFileSync('site/data/citekeyformatters/functions.json', stringify(api.doc.function, null, 2))
fs.writeFileSync('site/data/citekeyformatters/filters.json', stringify(api.doc.filter, null, 2))