Rename typecheck.ts to intergen.ts
This commit is contained in:
parent
cca888641a
commit
25593dd994
@ -1,275 +1,365 @@
|
|||||||
import * as path from 'path'
|
import * as fs from 'fs'
|
||||||
import * as ts from 'typescript'
|
import * as ts from 'typescript'
|
||||||
import {readFileSync} from 'fs'
|
import {argparse, arg} from '@rondo/argparse'
|
||||||
|
import {error, info} from '../log'
|
||||||
|
|
||||||
// function processLiteral(type: ts.TypeLiteralNode): string {
|
function isObjectType(type: ts.Type): type is ts.ObjectType {
|
||||||
// switch (type
|
return !!(type.flags & ts.TypeFlags.Object)
|
||||||
// }
|
|
||||||
|
|
||||||
function processTypeArguments(
|
|
||||||
typeArguments?: ts.NodeArray<ts.TypeNode>,
|
|
||||||
): string {
|
|
||||||
if (!typeArguments) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return '<' + typeArguments.map(processTypes).join(', ') + '>'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processTypeParameters(
|
function isTypeReference(type: ts.ObjectType): type is ts.TypeReference {
|
||||||
typeParameters?: ts.NodeArray<ts.TypeParameterDeclaration>,
|
return !!(type.objectFlags & ts.ObjectFlags.Reference)
|
||||||
): string {
|
}
|
||||||
if (!typeParameters) {
|
|
||||||
return ''
|
function filterInvisibleProperties(type: ts.Symbol): boolean {
|
||||||
|
const flags = ts.getCombinedModifierFlags(type.valueDeclaration)
|
||||||
|
return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IClassProperty {
|
||||||
|
name: string
|
||||||
|
type: ts.Type
|
||||||
|
relevantTypes: ts.Type[]
|
||||||
|
typeString: string
|
||||||
|
optional: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IClassDefinition {
|
||||||
|
name: string
|
||||||
|
type: ts.Type
|
||||||
|
typeParameters: ts.TypeParameter[]
|
||||||
|
relevantTypeParameters: ts.Type[]
|
||||||
|
allRelevantTypes: ts.Type[]
|
||||||
|
properties: IClassProperty[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO
|
||||||
|
*
|
||||||
|
* Interfaces generated from exported class delcarations will be prefixed with
|
||||||
|
* "I". A few cases need to be differentiated:
|
||||||
|
*
|
||||||
|
* a) Private (non-exported) types / interfaces / classes defined and used in
|
||||||
|
* same module. In case of non-exported classes, an error can be thrown.
|
||||||
|
* These can be copied and perhaps indexed to prevent collisions.
|
||||||
|
* b) Referenced exported classes from the same file
|
||||||
|
* c) Referenced exported classes from a neighbouring file
|
||||||
|
* d) Referenced imported classes from external modules. Real world example:
|
||||||
|
* entities in @rondo/comments-server import and use entities from
|
||||||
|
* @rondo/comments. These types will have to be processed by this module.
|
||||||
|
* e) Referenced interfaces should be re-imported in the output file.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function typecheck(...argv: string[]) {
|
||||||
|
const args = argparse({
|
||||||
|
input: arg('string', {alias: 'i', required: true}),
|
||||||
|
debug: arg('boolean'),
|
||||||
|
help: arg('boolean', {alias: 'h'}),
|
||||||
|
output: arg('string', {default: '-'}),
|
||||||
|
}).parse(argv)
|
||||||
|
|
||||||
|
function debug(m: string, ...meta: any[]) {
|
||||||
|
if (args.debug) {
|
||||||
|
error(m, ...meta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return '<' + typeParameters
|
|
||||||
.map(tp => ({
|
/** Generate interfaces for all exported classes in a set of .ts files */
|
||||||
name: tp.name.text,
|
function classesToInterfaces(
|
||||||
constraint: tp.constraint
|
fileNames: string[],
|
||||||
? processTypes(tp.constraint)
|
options: ts.CompilerOptions,
|
||||||
: undefined,
|
): string[] {
|
||||||
default: tp.default
|
// Build a program using the set of root file names in fileNames
|
||||||
? processTypes(tp.default)
|
const program = ts.createProgram(fileNames, options)
|
||||||
: undefined,
|
|
||||||
}))
|
// Get the checker, we will use it to find more about classes
|
||||||
.map(tp => {
|
const checker = program.getTypeChecker()
|
||||||
if (tp.constraint && tp.default) {
|
|
||||||
return `${tp.name} extends ${tp.constraint} = ${tp.default}`
|
const classDefs: IClassDefinition[] = []
|
||||||
|
|
||||||
|
function typeToString(type: ts.Type): string {
|
||||||
|
return checker.typeToString(type)
|
||||||
}
|
}
|
||||||
if (tp.constraint) {
|
|
||||||
return `${tp.name} extends ${tp.constraint}`
|
/**
|
||||||
|
* Can be used to filters out global types like Array or string from a list
|
||||||
|
* of types. For example: types.filter(filterGlobalTypes)
|
||||||
|
*/
|
||||||
|
function filterGlobalTypes(type: ts.Type): boolean {
|
||||||
|
debug('filterGlobalTypes: %s', typeToString(type))
|
||||||
|
if (type.aliasSymbol) {
|
||||||
|
// keep type aliases
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const symbol = type.getSymbol()
|
||||||
|
if (!symbol) {
|
||||||
|
debug(' no symbol')
|
||||||
|
// e.g. string or number types have no symbol
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (symbol && symbol.flags & ts.SymbolFlags.Transient) {
|
||||||
|
debug(' is transient')
|
||||||
|
// Array is transient. not sure if this is the best way to figure this
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// if (symbol && !((symbol as any).parent)) {
|
||||||
|
// // debug(' no parent', symbol)
|
||||||
|
// // e.g. Array symbol has no parent
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
if (type.isLiteral()) {
|
||||||
|
debug(' is literal')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (type.isUnionOrIntersection()) {
|
||||||
|
debug(' is u or i')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isObjectType(type) && isTypeReference(type)) {
|
||||||
|
debug(' is object type')
|
||||||
|
if (isObjectType(type.target) &&
|
||||||
|
type.target.objectFlags & ts.ObjectFlags.Tuple) {
|
||||||
|
debug(' is tuple')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(' keep!')
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if (tp.default) {
|
|
||||||
return `${tp.name} = ${tp.default}`
|
/**
|
||||||
|
* Converts a generic type to the target of the type reference.
|
||||||
|
*/
|
||||||
|
function mapGenericTypes(type: ts.Type): ts.Type {
|
||||||
|
if (type.aliasSymbol) {
|
||||||
|
return checker.getDeclaredTypeOfSymbol(type.aliasSymbol)
|
||||||
|
}
|
||||||
|
if (isObjectType(type) && isTypeReference(type)) {
|
||||||
|
return type.target
|
||||||
|
}
|
||||||
|
return type
|
||||||
}
|
}
|
||||||
return tp.name
|
|
||||||
|
/**
|
||||||
|
* Removes duplicates from an array of types
|
||||||
|
*/
|
||||||
|
function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) {
|
||||||
|
// TODO improve performance of this method
|
||||||
|
return i === arr.indexOf(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively retrieves a list of all type parameters.
|
||||||
|
*/
|
||||||
|
function getAllTypeParameters(type: ts.Type): ts.Type[] {
|
||||||
|
function collectTypeParams(
|
||||||
|
type2: ts.Type, params?: readonly ts.Type[],
|
||||||
|
): ts.Type[] {
|
||||||
|
const types: ts.Type[] = [type2]
|
||||||
|
if (params) {
|
||||||
|
params.forEach(t => {
|
||||||
|
const atp = getAllTypeParameters(t)
|
||||||
|
types.push(...atp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.aliasSymbol) {
|
||||||
|
return collectTypeParams(type, type.aliasTypeArguments)
|
||||||
|
}
|
||||||
|
if (isObjectType(type) && isTypeReference(type)) {
|
||||||
|
return collectTypeParams(type, type.typeArguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isUnionOrIntersection()) {
|
||||||
|
return collectTypeParams(type, type.types)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isClassOrInterface()) {
|
||||||
|
return collectTypeParams(type, type.typeParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [type]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this is visible outside this file, false otherwise
|
||||||
|
*/
|
||||||
|
function isNodeExported(node: ts.Node): boolean {
|
||||||
|
return (
|
||||||
|
(ts.getCombinedModifierFlags(node as any) &
|
||||||
|
ts.ModifierFlags.Export) !== 0 ||
|
||||||
|
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClassDeclaration(node: ts.ClassDeclaration) {
|
||||||
|
if (!node.name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// This is a top level class, get its symbol
|
||||||
|
const symbol = checker.getSymbolAtLocation(node.name)
|
||||||
|
if (!symbol) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = checker.getDeclaredTypeOfSymbol(symbol)
|
||||||
|
handleType(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeDefinitions: Map<ts.Type, IClassDefinition> = new Map()
|
||||||
|
function handleType(type: ts.Type) {
|
||||||
|
if (typeDefinitions.has(type)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type.aliasSymbol) {
|
||||||
|
throw new Error('Type aliases are not supported')
|
||||||
|
}
|
||||||
|
const typeParameters: ts.TypeParameter[] = []
|
||||||
|
const expandedTypeParameters: ts.Type[] = []
|
||||||
|
const allRelevantTypes: ts.Type[] = []
|
||||||
|
|
||||||
|
function handleTypeParameters(typeParams: readonly ts.Type[]) {
|
||||||
|
typeParams.forEach(tp => {
|
||||||
|
const constraint = tp.getConstraint()
|
||||||
|
if (constraint) {
|
||||||
|
expandedTypeParameters.push(...getAllTypeParameters(tp))
|
||||||
|
}
|
||||||
|
const def = tp.getDefault()
|
||||||
|
if (def) {
|
||||||
|
expandedTypeParameters.push(...getAllTypeParameters(tp))
|
||||||
|
}
|
||||||
|
|
||||||
|
typeParameters.push(tp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isClassOrInterface() && type.typeParameters) {
|
||||||
|
handleTypeParameters(type.typeParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.aliasSymbol && type.aliasTypeArguments) {
|
||||||
|
handleTypeParameters(type.aliasTypeArguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties = type.getApparentProperties()
|
||||||
|
|
||||||
|
const filterClassTypeParameters =
|
||||||
|
(t: ts.Type) => typeParameters.every(tp => tp !== t)
|
||||||
|
|
||||||
|
const classProperties: IClassProperty[] = properties
|
||||||
|
.filter(filterInvisibleProperties)
|
||||||
|
.map(p => {
|
||||||
|
const vd = p.valueDeclaration
|
||||||
|
const optional = ts.isPropertyDeclaration(vd) && !!vd.questionToken
|
||||||
|
|
||||||
|
const propType = checker.getTypeOfSymbolAtLocation(p, vd)
|
||||||
|
|
||||||
|
const typeParams = getAllTypeParameters(propType)
|
||||||
|
|
||||||
|
const relevantTypes = typeParams
|
||||||
|
.filter(filterGlobalTypes)
|
||||||
|
.filter(filterClassTypeParameters)
|
||||||
|
.map(mapGenericTypes)
|
||||||
|
.filter(filterDuplicates)
|
||||||
|
|
||||||
|
allRelevantTypes.push(...relevantTypes)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: p.getName(),
|
||||||
|
type: propType,
|
||||||
|
relevantTypes,
|
||||||
|
typeString: typeToString(propType),
|
||||||
|
optional,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const relevantTypeParameters = expandedTypeParameters
|
||||||
|
.filter(filterGlobalTypes)
|
||||||
|
.filter(mapGenericTypes)
|
||||||
|
.filter(filterDuplicates)
|
||||||
|
|
||||||
|
allRelevantTypes.push(...relevantTypeParameters)
|
||||||
|
|
||||||
|
const classDef: IClassDefinition = {
|
||||||
|
name: typeToString(type),
|
||||||
|
type,
|
||||||
|
// name: symbol.getName(),
|
||||||
|
typeParameters,
|
||||||
|
allRelevantTypes: allRelevantTypes
|
||||||
|
.filter(filterClassTypeParameters)
|
||||||
|
.filter(filterDuplicates),
|
||||||
|
relevantTypeParameters,
|
||||||
|
properties: classProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
classDefs.push(classDef)
|
||||||
|
typeDefinitions.set(type, classDef)
|
||||||
|
|
||||||
|
classDef.allRelevantTypes.forEach(handleType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit nodes finding exported classes
|
||||||
|
*/
|
||||||
|
function visit(node: ts.Node) {
|
||||||
|
// Only consider exported nodes
|
||||||
|
if (!isNodeExported(node)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isClassDeclaration(node)) {
|
||||||
|
handleClassDeclaration(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit every sourceFile in the program
|
||||||
|
for (const sourceFile of program.getSourceFiles()) {
|
||||||
|
if (!sourceFile.isDeclarationFile) {
|
||||||
|
// Walk the tree to search for classes
|
||||||
|
ts.forEachChild(sourceFile, visit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTypeName(type: ts.Type, mappings: Map<ts.Type, string>) {
|
||||||
|
const name = typeToString(type)
|
||||||
|
mappings.set(type, `I${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameMappings = new Map<ts.Type, string>()
|
||||||
|
for (const classDef of classDefs) {
|
||||||
|
setTypeName(classDef.type, nameMappings)
|
||||||
|
for (const t of classDef.allRelevantTypes) {
|
||||||
|
setTypeName(classDef.type, nameMappings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInterface(classDef: IClassDefinition): string {
|
||||||
|
const name = nameMappings.get(classDef.type)!
|
||||||
|
const start = `interface ${name} {`
|
||||||
|
const properties = classDef.properties.map(p => {
|
||||||
|
return ` ${p.name}: ${nameMappings.get(p.type) || p.typeString}`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
const end = '}'
|
||||||
|
return `${start}\n${properties}\n${end}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return classDefs.map(createInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
const interfaces = classesToInterfaces([args.input], {
|
||||||
|
target: ts.ScriptTarget.ES5,
|
||||||
|
module: ts.ModuleKind.CommonJS,
|
||||||
})
|
})
|
||||||
.join(', ') + '>'
|
|
||||||
}
|
|
||||||
|
|
||||||
function processLiteral(
|
const value = interfaces.join('\n\n')
|
||||||
literal: ts.BooleanLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression,
|
if (args.output === '-') {
|
||||||
) {
|
info(value)
|
||||||
switch (literal.kind) {
|
} else {
|
||||||
case ts.SyntaxKind.TrueKeyword:
|
fs.writeFileSync(args.output, value)
|
||||||
return '\'true\''
|
|
||||||
case ts.SyntaxKind.FalseKeyword:
|
|
||||||
return '\'false\''
|
|
||||||
default:
|
|
||||||
if (ts.isLiteralExpression(literal)) {
|
|
||||||
return `'${literal.text}'`
|
|
||||||
}
|
|
||||||
throw new Error('Unsupported literal type: ' + literal.kind)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processTypes(type: ts.TypeNode): string {
|
|
||||||
switch (type.kind) {
|
|
||||||
case ts.SyntaxKind.StringKeyword:
|
|
||||||
return 'string'
|
|
||||||
case ts.SyntaxKind.BooleanKeyword:
|
|
||||||
return 'boolean'
|
|
||||||
case ts.SyntaxKind.NumberKeyword:
|
|
||||||
return 'number'
|
|
||||||
case ts.SyntaxKind.NullKeyword:
|
|
||||||
return 'null'
|
|
||||||
case ts.SyntaxKind.TrueKeyword:
|
|
||||||
return '\'true\''
|
|
||||||
case ts.SyntaxKind.FalseKeyword:
|
|
||||||
return '\'false\''
|
|
||||||
case ts.SyntaxKind.LiteralType:
|
|
||||||
const literalType = type as ts.LiteralTypeNode
|
|
||||||
return processLiteral(literalType.literal)
|
|
||||||
// return literalType.literal.text
|
|
||||||
case ts.SyntaxKind.TypeReference:
|
|
||||||
const typeRef = type as ts.TypeReferenceNode
|
|
||||||
typeRef.typeArguments
|
|
||||||
// FIXME do not use any
|
|
||||||
return (typeRef.typeName as any).escapedText + processTypeArguments(typeRef.typeArguments)
|
|
||||||
case ts.SyntaxKind.TypeLiteral:
|
|
||||||
const typeLiteral = type as ts.TypeLiteralNode
|
|
||||||
return '{' + processInterfaceMembers(typeLiteral.members).join('\n') + '}'
|
|
||||||
// typeLiteral.members.map(processTypes)
|
|
||||||
// console.log('aaa', JSON.stringify(typeLiteral, null, ' '))
|
|
||||||
// return 'type literal...'
|
|
||||||
// console.log(' ', 'type literal...')
|
|
||||||
// TODO recursively iterate through typeLiteral.members
|
|
||||||
break
|
|
||||||
case ts.SyntaxKind.UnionType:
|
|
||||||
const unionType = type as ts.UnionTypeNode
|
|
||||||
const unionTypes = unionType.types.map(processTypes).join(' | ')
|
|
||||||
return unionTypes
|
|
||||||
case ts.SyntaxKind.TupleType:
|
|
||||||
const tupleTypeNode = type as ts.TupleTypeNode
|
|
||||||
const tupleTypes = tupleTypeNode.elementTypes.map(processTypes).join(', ')
|
|
||||||
return `[${tupleTypes}]`
|
|
||||||
// TODO recursively iterate through tupleTypeNode.elementTypes
|
|
||||||
break
|
|
||||||
case ts.SyntaxKind.ArrayType:
|
|
||||||
const arrayType = type as ts.ArrayTypeNode
|
|
||||||
return `Array<${processTypes(arrayType.elementType)}>`
|
|
||||||
default:
|
|
||||||
throw new Error('unhandled type: ' + type.kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processInterfaceMembers(
|
|
||||||
members: ts.NodeArray<ts.TypeElement>,
|
|
||||||
): string[] {
|
|
||||||
const results: string[] = []
|
|
||||||
|
|
||||||
for (const m of members) {
|
|
||||||
if (m.kind !== ts.SyntaxKind.PropertySignature) {
|
|
||||||
throw new Error('not implemented support for node.kind: ' + m.kind)
|
|
||||||
}
|
|
||||||
const member = m as ts.PropertySignature
|
|
||||||
const name = (m.name as ts.Identifier).escapedText
|
|
||||||
|
|
||||||
let result = ''
|
|
||||||
let readonly = false
|
|
||||||
if (m.modifiers) {
|
|
||||||
for (const modifier of m.modifiers) {
|
|
||||||
if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) {
|
|
||||||
readonly = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' modifier.kind:', modifier.kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readonly) {
|
|
||||||
result += 'readonly ' + name
|
|
||||||
} else {
|
|
||||||
result += name
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!member.type) {
|
|
||||||
throw new Error('No member type!')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member.questionToken) {
|
|
||||||
result += '?: '
|
|
||||||
} else {
|
|
||||||
result += ': '
|
|
||||||
}
|
|
||||||
|
|
||||||
result += processTypes(member.type)
|
|
||||||
results.push(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
function processClassMembers(
|
|
||||||
members: ts.NodeArray<ts.ClassElement>,
|
|
||||||
): string[] {
|
|
||||||
const results: string[] = []
|
|
||||||
|
|
||||||
for (const m of members) {
|
|
||||||
if (m.kind !== ts.SyntaxKind.PropertyDeclaration) {
|
|
||||||
throw new Error('not implemented support for node.kind: ' + m.kind)
|
|
||||||
}
|
|
||||||
const member = m as ts.PropertyDeclaration
|
|
||||||
const name = (m.name as ts.Identifier).escapedText
|
|
||||||
|
|
||||||
let result = ''
|
|
||||||
let readonly = false
|
|
||||||
let skip = false
|
|
||||||
if (m.modifiers) {
|
|
||||||
for (const modifier of m.modifiers) {
|
|
||||||
if (modifier.kind === ts.SyntaxKind.PrivateKeyword) {
|
|
||||||
skip = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (modifier.kind === ts.SyntaxKind.ProtectedKeyword) {
|
|
||||||
skip = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) {
|
|
||||||
readonly = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' modifier.kind:', modifier.kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skip) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (readonly) {
|
|
||||||
result += 'readonly ' + name
|
|
||||||
} else {
|
|
||||||
result += name
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!member.type) {
|
|
||||||
throw new Error('No member type!')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member.questionToken) {
|
|
||||||
result += '?: '
|
|
||||||
} else {
|
|
||||||
result += ': '
|
|
||||||
}
|
|
||||||
|
|
||||||
result += processTypes(member.type)
|
|
||||||
results.push(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
function delint(sourceFile: ts.SourceFile) {
|
|
||||||
delintNode(sourceFile)
|
|
||||||
|
|
||||||
function delintNode(node: ts.Node) {
|
|
||||||
// TODO check which classes are exported
|
|
||||||
switch (node.kind) {
|
|
||||||
case ts.SyntaxKind.InterfaceDeclaration:
|
|
||||||
const interfaceDeclaration = node as ts.InterfaceDeclaration
|
|
||||||
let ifaceName = interfaceDeclaration.name.text
|
|
||||||
if (interfaceDeclaration.typeParameters) {
|
|
||||||
ifaceName += processTypeParameters(interfaceDeclaration.typeParameters)
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
ifaceName,
|
|
||||||
processInterfaceMembers(interfaceDeclaration.members))
|
|
||||||
break
|
|
||||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
||||||
const typeAlias = node as ts.TypeAliasDeclaration
|
|
||||||
let taName = typeAlias.name.text
|
|
||||||
if (typeAlias.typeParameters) {
|
|
||||||
taName += processTypeParameters(typeAlias.typeParameters)
|
|
||||||
}
|
|
||||||
console.log(taName)
|
|
||||||
break
|
|
||||||
case ts.SyntaxKind.ClassDeclaration:
|
|
||||||
const cls = node as ts.ClassDeclaration
|
|
||||||
if (!cls.name) {
|
|
||||||
// TODO warn
|
|
||||||
throw new Error('no class name: ' + cls.pos)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let clsName = cls.name.text
|
|
||||||
if (cls.typeParameters) {
|
|
||||||
clsName += processTypeParameters(cls.typeParameters)
|
|
||||||
}
|
|
||||||
console.log(clsName, processClassMembers(cls.members))
|
|
||||||
}
|
|
||||||
ts.forEachChild(node, delintNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function intergen(argv: string[]) {
|
|
||||||
const filename = path.join(__dirname, 'intergen.sample.ts')
|
|
||||||
const sourceFile = ts.createSourceFile(
|
|
||||||
filename,
|
|
||||||
readFileSync(filename).toString(),
|
|
||||||
ts.ScriptTarget.ES2015,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
delint(sourceFile)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,365 +0,0 @@
|
|||||||
import * as fs from 'fs'
|
|
||||||
import * as ts from 'typescript'
|
|
||||||
import {argparse, arg} from '@rondo/argparse'
|
|
||||||
import {error, info} from '../log'
|
|
||||||
|
|
||||||
function isObjectType(type: ts.Type): type is ts.ObjectType {
|
|
||||||
return !!(type.flags & ts.TypeFlags.Object)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTypeReference(type: ts.ObjectType): type is ts.TypeReference {
|
|
||||||
return !!(type.objectFlags & ts.ObjectFlags.Reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterInvisibleProperties(type: ts.Symbol): boolean {
|
|
||||||
const flags = ts.getCombinedModifierFlags(type.valueDeclaration)
|
|
||||||
return !(flags & ts.ModifierFlags.NonPublicAccessibilityModifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IClassProperty {
|
|
||||||
name: string
|
|
||||||
type: ts.Type
|
|
||||||
relevantTypes: ts.Type[]
|
|
||||||
typeString: string
|
|
||||||
optional: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IClassDefinition {
|
|
||||||
name: string
|
|
||||||
type: ts.Type
|
|
||||||
typeParameters: ts.TypeParameter[]
|
|
||||||
relevantTypeParameters: ts.Type[]
|
|
||||||
allRelevantTypes: ts.Type[]
|
|
||||||
properties: IClassProperty[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO
|
|
||||||
*
|
|
||||||
* Interfaces generated from exported class delcarations will be prefixed with
|
|
||||||
* "I". A few cases need to be differentiated:
|
|
||||||
*
|
|
||||||
* a) Private (non-exported) types / interfaces / classes defined and used in
|
|
||||||
* same module. In case of non-exported classes, an error can be thrown.
|
|
||||||
* These can be copied and perhaps indexed to prevent collisions.
|
|
||||||
* b) Referenced exported classes from the same file
|
|
||||||
* c) Referenced exported classes from a neighbouring file
|
|
||||||
* d) Referenced imported classes from external modules. Real world example:
|
|
||||||
* entities in @rondo/comments-server import and use entities from
|
|
||||||
* @rondo/comments. These types will have to be processed by this module.
|
|
||||||
* e) Referenced interfaces should be re-imported in the output file.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function typecheck(...argv: string[]) {
|
|
||||||
const args = argparse({
|
|
||||||
input: arg('string', {alias: 'i', required: true}),
|
|
||||||
debug: arg('boolean'),
|
|
||||||
help: arg('boolean', {alias: 'h'}),
|
|
||||||
output: arg('string', {default: '-'}),
|
|
||||||
}).parse(argv)
|
|
||||||
|
|
||||||
function debug(m: string, ...meta: any[]) {
|
|
||||||
if (args.debug) {
|
|
||||||
error(m, ...meta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generate interfaces for all exported classes in a set of .ts files */
|
|
||||||
function classesToInterfaces(
|
|
||||||
fileNames: string[],
|
|
||||||
options: ts.CompilerOptions,
|
|
||||||
): string[] {
|
|
||||||
// Build a program using the set of root file names in fileNames
|
|
||||||
const program = ts.createProgram(fileNames, options)
|
|
||||||
|
|
||||||
// Get the checker, we will use it to find more about classes
|
|
||||||
const checker = program.getTypeChecker()
|
|
||||||
|
|
||||||
const classDefs: IClassDefinition[] = []
|
|
||||||
|
|
||||||
function typeToString(type: ts.Type): string {
|
|
||||||
return checker.typeToString(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be used to filters out global types like Array or string from a list
|
|
||||||
* of types. For example: types.filter(filterGlobalTypes)
|
|
||||||
*/
|
|
||||||
function filterGlobalTypes(type: ts.Type): boolean {
|
|
||||||
debug('filterGlobalTypes: %s', typeToString(type))
|
|
||||||
if (type.aliasSymbol) {
|
|
||||||
// keep type aliases
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const symbol = type.getSymbol()
|
|
||||||
if (!symbol) {
|
|
||||||
debug(' no symbol')
|
|
||||||
// e.g. string or number types have no symbol
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (symbol && symbol.flags & ts.SymbolFlags.Transient) {
|
|
||||||
debug(' is transient')
|
|
||||||
// Array is transient. not sure if this is the best way to figure this
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// if (symbol && !((symbol as any).parent)) {
|
|
||||||
// // debug(' no parent', symbol)
|
|
||||||
// // e.g. Array symbol has no parent
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
if (type.isLiteral()) {
|
|
||||||
debug(' is literal')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (type.isUnionOrIntersection()) {
|
|
||||||
debug(' is u or i')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (isObjectType(type) && isTypeReference(type)) {
|
|
||||||
debug(' is object type')
|
|
||||||
if (isObjectType(type.target) &&
|
|
||||||
type.target.objectFlags & ts.ObjectFlags.Tuple) {
|
|
||||||
debug(' is tuple')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(' keep!')
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a generic type to the target of the type reference.
|
|
||||||
*/
|
|
||||||
function mapGenericTypes(type: ts.Type): ts.Type {
|
|
||||||
if (type.aliasSymbol) {
|
|
||||||
return checker.getDeclaredTypeOfSymbol(type.aliasSymbol)
|
|
||||||
}
|
|
||||||
if (isObjectType(type) && isTypeReference(type)) {
|
|
||||||
return type.target
|
|
||||||
}
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes duplicates from an array of types
|
|
||||||
*/
|
|
||||||
function filterDuplicates(type: ts.Type, i: number, arr: ts.Type[]) {
|
|
||||||
// TODO improve performance of this method
|
|
||||||
return i === arr.indexOf(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively retrieves a list of all type parameters.
|
|
||||||
*/
|
|
||||||
function getAllTypeParameters(type: ts.Type): ts.Type[] {
|
|
||||||
function collectTypeParams(
|
|
||||||
type2: ts.Type, params?: readonly ts.Type[],
|
|
||||||
): ts.Type[] {
|
|
||||||
const types: ts.Type[] = [type2]
|
|
||||||
if (params) {
|
|
||||||
params.forEach(t => {
|
|
||||||
const atp = getAllTypeParameters(t)
|
|
||||||
types.push(...atp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.aliasSymbol) {
|
|
||||||
return collectTypeParams(type, type.aliasTypeArguments)
|
|
||||||
}
|
|
||||||
if (isObjectType(type) && isTypeReference(type)) {
|
|
||||||
return collectTypeParams(type, type.typeArguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.isUnionOrIntersection()) {
|
|
||||||
return collectTypeParams(type, type.types)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.isClassOrInterface()) {
|
|
||||||
return collectTypeParams(type, type.typeParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [type]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if this is visible outside this file, false otherwise
|
|
||||||
*/
|
|
||||||
function isNodeExported(node: ts.Node): boolean {
|
|
||||||
return (
|
|
||||||
(ts.getCombinedModifierFlags(node as any) &
|
|
||||||
ts.ModifierFlags.Export) !== 0 ||
|
|
||||||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClassDeclaration(node: ts.ClassDeclaration) {
|
|
||||||
if (!node.name) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// This is a top level class, get its symbol
|
|
||||||
const symbol = checker.getSymbolAtLocation(node.name)
|
|
||||||
if (!symbol) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = checker.getDeclaredTypeOfSymbol(symbol)
|
|
||||||
handleType(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeDefinitions: Map<ts.Type, IClassDefinition> = new Map()
|
|
||||||
function handleType(type: ts.Type) {
|
|
||||||
if (typeDefinitions.has(type)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (type.aliasSymbol) {
|
|
||||||
throw new Error('Type aliases are not supported')
|
|
||||||
}
|
|
||||||
const typeParameters: ts.TypeParameter[] = []
|
|
||||||
const expandedTypeParameters: ts.Type[] = []
|
|
||||||
const allRelevantTypes: ts.Type[] = []
|
|
||||||
|
|
||||||
function handleTypeParameters(typeParams: readonly ts.Type[]) {
|
|
||||||
typeParams.forEach(tp => {
|
|
||||||
const constraint = tp.getConstraint()
|
|
||||||
if (constraint) {
|
|
||||||
expandedTypeParameters.push(...getAllTypeParameters(tp))
|
|
||||||
}
|
|
||||||
const def = tp.getDefault()
|
|
||||||
if (def) {
|
|
||||||
expandedTypeParameters.push(...getAllTypeParameters(tp))
|
|
||||||
}
|
|
||||||
|
|
||||||
typeParameters.push(tp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.isClassOrInterface() && type.typeParameters) {
|
|
||||||
handleTypeParameters(type.typeParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type.aliasSymbol && type.aliasTypeArguments) {
|
|
||||||
handleTypeParameters(type.aliasTypeArguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
const properties = type.getApparentProperties()
|
|
||||||
|
|
||||||
const filterClassTypeParameters =
|
|
||||||
(t: ts.Type) => typeParameters.every(tp => tp !== t)
|
|
||||||
|
|
||||||
const classProperties: IClassProperty[] = properties
|
|
||||||
.filter(filterInvisibleProperties)
|
|
||||||
.map(p => {
|
|
||||||
const vd = p.valueDeclaration
|
|
||||||
const optional = ts.isPropertyDeclaration(vd) && !!vd.questionToken
|
|
||||||
|
|
||||||
const propType = checker.getTypeOfSymbolAtLocation(p, vd)
|
|
||||||
|
|
||||||
const typeParams = getAllTypeParameters(propType)
|
|
||||||
|
|
||||||
const relevantTypes = typeParams
|
|
||||||
.filter(filterGlobalTypes)
|
|
||||||
.filter(filterClassTypeParameters)
|
|
||||||
.map(mapGenericTypes)
|
|
||||||
.filter(filterDuplicates)
|
|
||||||
|
|
||||||
allRelevantTypes.push(...relevantTypes)
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: p.getName(),
|
|
||||||
type: propType,
|
|
||||||
relevantTypes,
|
|
||||||
typeString: typeToString(propType),
|
|
||||||
optional,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const relevantTypeParameters = expandedTypeParameters
|
|
||||||
.filter(filterGlobalTypes)
|
|
||||||
.filter(mapGenericTypes)
|
|
||||||
.filter(filterDuplicates)
|
|
||||||
|
|
||||||
allRelevantTypes.push(...relevantTypeParameters)
|
|
||||||
|
|
||||||
const classDef: IClassDefinition = {
|
|
||||||
name: typeToString(type),
|
|
||||||
type,
|
|
||||||
// name: symbol.getName(),
|
|
||||||
typeParameters,
|
|
||||||
allRelevantTypes: allRelevantTypes
|
|
||||||
.filter(filterClassTypeParameters)
|
|
||||||
.filter(filterDuplicates),
|
|
||||||
relevantTypeParameters,
|
|
||||||
properties: classProperties,
|
|
||||||
}
|
|
||||||
|
|
||||||
classDefs.push(classDef)
|
|
||||||
typeDefinitions.set(type, classDef)
|
|
||||||
|
|
||||||
classDef.allRelevantTypes.forEach(handleType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visit nodes finding exported classes
|
|
||||||
*/
|
|
||||||
function visit(node: ts.Node) {
|
|
||||||
// Only consider exported nodes
|
|
||||||
if (!isNodeExported(node)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ts.isClassDeclaration(node)) {
|
|
||||||
handleClassDeclaration(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visit every sourceFile in the program
|
|
||||||
for (const sourceFile of program.getSourceFiles()) {
|
|
||||||
if (!sourceFile.isDeclarationFile) {
|
|
||||||
// Walk the tree to search for classes
|
|
||||||
ts.forEachChild(sourceFile, visit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTypeName(type: ts.Type, mappings: Map<ts.Type, string>) {
|
|
||||||
const name = typeToString(type)
|
|
||||||
mappings.set(type, `I${name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameMappings = new Map<ts.Type, string>()
|
|
||||||
for (const classDef of classDefs) {
|
|
||||||
setTypeName(classDef.type, nameMappings)
|
|
||||||
for (const t of classDef.allRelevantTypes) {
|
|
||||||
setTypeName(classDef.type, nameMappings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createInterface(classDef: IClassDefinition): string {
|
|
||||||
const name = nameMappings.get(classDef.type)!
|
|
||||||
const start = `interface ${name} {`
|
|
||||||
const properties = classDef.properties.map(p => {
|
|
||||||
return ` ${p.name}: ${nameMappings.get(p.type) || p.typeString}`
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
const end = '}'
|
|
||||||
return `${start}\n${properties}\n${end}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return classDefs.map(createInterface)
|
|
||||||
}
|
|
||||||
|
|
||||||
const interfaces = classesToInterfaces([args.input], {
|
|
||||||
target: ts.ScriptTarget.ES5,
|
|
||||||
module: ts.ModuleKind.CommonJS,
|
|
||||||
})
|
|
||||||
|
|
||||||
const value = interfaces.join('\n\n')
|
|
||||||
if (args.output === '-') {
|
|
||||||
info(value)
|
|
||||||
} else {
|
|
||||||
fs.writeFileSync(args.output, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user