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 {readFileSync} from 'fs'
|
||||
import {argparse, arg} from '@rondo/argparse'
|
||||
import {error, info} from '../log'
|
||||
|
||||
// function processLiteral(type: ts.TypeLiteralNode): string {
|
||||
// switch (type
|
||||
// }
|
||||
|
||||
function processTypeArguments(
|
||||
typeArguments?: ts.NodeArray<ts.TypeNode>,
|
||||
): string {
|
||||
if (!typeArguments) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return '<' + typeArguments.map(processTypes).join(', ') + '>'
|
||||
function isObjectType(type: ts.Type): type is ts.ObjectType {
|
||||
return !!(type.flags & ts.TypeFlags.Object)
|
||||
}
|
||||
|
||||
function processTypeParameters(
|
||||
typeParameters?: ts.NodeArray<ts.TypeParameterDeclaration>,
|
||||
): string {
|
||||
if (!typeParameters) {
|
||||
return ''
|
||||
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)
|
||||
}
|
||||
}
|
||||
return '<' + typeParameters
|
||||
.map(tp => ({
|
||||
name: tp.name.text,
|
||||
constraint: tp.constraint
|
||||
? processTypes(tp.constraint)
|
||||
: undefined,
|
||||
default: tp.default
|
||||
? processTypes(tp.default)
|
||||
: undefined,
|
||||
}))
|
||||
.map(tp => {
|
||||
if (tp.constraint && tp.default) {
|
||||
return `${tp.name} extends ${tp.constraint} = ${tp.default}`
|
||||
|
||||
/** 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)
|
||||
}
|
||||
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(
|
||||
literal: ts.BooleanLiteral | ts.LiteralExpression | ts.PrefixUnaryExpression,
|
||||
) {
|
||||
switch (literal.kind) {
|
||||
case ts.SyntaxKind.TrueKeyword:
|
||||
return '\'true\''
|
||||
case ts.SyntaxKind.FalseKeyword:
|
||||
return '\'false\''
|
||||
default:
|
||||
if (ts.isLiteralExpression(literal)) {
|
||||
return `'${literal.text}'`
|
||||
}
|
||||
throw new Error('Unsupported literal type: ' + literal.kind)
|
||||
const value = interfaces.join('\n\n')
|
||||
if (args.output === '-') {
|
||||
info(value)
|
||||
} else {
|
||||
fs.writeFileSync(args.output, value)
|
||||
}
|
||||
}
|
||||
|
||||
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